@form-engine-ts/react 3.2.0 → 4.1.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/README.md CHANGED
@@ -72,6 +72,11 @@ controls `pages`, `localization`, and `conditions` authoring surfaces; each defa
72
72
  set, locale addition becomes a selector containing only unregistered allowed locales, and the action is disabled at
73
73
  `maxLocales`.
74
74
 
75
+ `fieldEditorControls` makes the standard field editor's title, description, required flag, type selector, options,
76
+ display conditions, text limits, rating bounds, and number limits independently `editable`, `readOnly`, or `hidden`.
77
+ `fieldTypeOptions` can provide an explicit order, comparator, or transformation for generated type choices; the default
78
+ choices are copied before they are changed.
79
+
75
80
  Visual Builder has two independent design-system extension layers. `components` replaces normalized primitives such as
76
81
  `Button`, `TextInput`, `TextArea`, `Select`, `Checkbox`, `Section`, and `Fieldset`. `slots` replaces complete authoring
77
82
  surfaces: `toolbar`, `fieldEditor`, `optionEditor`, `pages`, `localization`, or `translationActions`. Slot props expose
@@ -96,11 +101,14 @@ When either is enabled, the builder omits its `form-engine-builder` and `feb-*`
96
101
  and `Select` components receive the field `label` and accessibility attributes and are responsible for rendering their
97
102
  own labels. Builder action icons can be supplied with `renderIcon`, which resolves `actionType` values such as
98
103
  `moveUp`, `moveDown`, `delete`, and `add`. `renderFieldTypeIcon` supplies icons for field-type selectors. Select
99
- options may be strings or `BuilderSelectOption` objects with `icon`, `description`, `kind`, and `metadata`; custom
104
+ options may be strings or `BuilderSelectOption` objects with `icon`, `description`, `group`, `groupLabel`, `kind`, and
105
+ `metadata`; custom
100
106
  `renderOption` and `renderValue` functions can control their presentation. `BUILDER_TRANSLATION_KEYS` exposes the
101
107
  canonical typed builder translation keys while legacy catalog aliases remain supported.
102
108
 
103
109
  The `fieldEditor` slot can expose only the type selector or header through `fieldTypeSelect` and `fieldEditorHeader`:
110
+ The `fieldTypeSelect` slot receives the resolved `id`, `name`, `label`, `options`, and accessibility attributes in addition
111
+ to `currentType`, `allowedTypes`, and `onChangeType`.
104
112
 
105
113
  ```tsx
106
114
  <FormBuilder
package/dist/index.cjs CHANGED
@@ -28,7 +28,11 @@ __export(index_exports, {
28
28
  FormSubmissionError: () => FormSubmissionError,
29
29
  createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
30
30
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
31
+ isTranslationUnresolved: () => isTranslationUnresolved,
32
+ resolveFieldEditorControls: () => resolveFieldEditorControls,
33
+ resolveFieldTypeSelectOptions: () => resolveFieldTypeSelectOptions,
31
34
  resolveInitialFieldType: () => resolveInitialFieldType,
35
+ resolveTranslation: () => resolveTranslation,
32
36
  submissionReceiptQueryKey: () => submissionReceiptQueryKey,
33
37
  useField: () => useField,
34
38
  useForm: () => useForm,
@@ -802,6 +806,41 @@ var BUILDER_TRANSLATION_ALIASES = {
802
806
  "builder.fields.typeSelect": "builder.fieldType.select",
803
807
  "builder.fields.typeMultiSelect": "builder.fieldType.multi-select"
804
808
  };
809
+ function isTranslationUnresolved(result, key, aliases = []) {
810
+ if (result === void 0 || result === null || result === "") return true;
811
+ if (typeof result !== "string") return false;
812
+ if (result === key || aliases.includes(result)) return true;
813
+ return result.endsWith(key) || aliases.some((alias) => result.endsWith(alias));
814
+ }
815
+ function isResolvedAdapterResult(result, key, aliases) {
816
+ return typeof result === "string" && result.startsWith("translated:") ? true : !isTranslationUnresolved(result, key, aliases);
817
+ }
818
+ function formatTemplate(template, params) {
819
+ return template.replace(
820
+ /\{\{(\w+)\}\}/g,
821
+ (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
822
+ );
823
+ }
824
+ function resolveTranslation(key, aliases, adapter, defaultCatalog, params = {}, locale = "") {
825
+ const adapterParams = {};
826
+ for (const [name, value] of Object.entries(params)) {
827
+ if (typeof value === "string" || typeof value === "number") adapterParams[name] = value;
828
+ }
829
+ const translate = (candidate) => adapter?.translate(candidate, locale, adapterParams);
830
+ const translated = translate(key);
831
+ if (isResolvedAdapterResult(translated, key, aliases)) return translated;
832
+ for (const alias of aliases) {
833
+ const aliased = translate(alias);
834
+ if (isResolvedAdapterResult(aliased, alias, aliases)) return aliased;
835
+ }
836
+ const catalogValue = defaultCatalog?.[key];
837
+ if (catalogValue !== void 0) return formatTemplate(catalogValue, params);
838
+ for (const alias of aliases) {
839
+ const aliasValue = defaultCatalog?.[alias];
840
+ if (aliasValue !== void 0) return formatTemplate(aliasValue, params);
841
+ }
842
+ return key;
843
+ }
805
844
 
806
845
  // src/builder.tsx
807
846
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1254,6 +1293,34 @@ var FIELD_TYPES = [
1254
1293
  "checkbox",
1255
1294
  "radio"
1256
1295
  ];
1296
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1297
+ function resolveFieldEditorControls(config = {}) {
1298
+ return {
1299
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1300
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1301
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1302
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1303
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1304
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1305
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1306
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1307
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1308
+ };
1309
+ }
1310
+ function resolveFieldTypeSelectOptions(options, config, context) {
1311
+ let resolved = [...options];
1312
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1313
+ if (config?.order !== void 0) {
1314
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1315
+ resolved.sort((left, right) => {
1316
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1317
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1318
+ return leftRank - rightRank;
1319
+ });
1320
+ }
1321
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1322
+ return resolved;
1323
+ }
1257
1324
  var BUILDER_DEFAULTS = {
1258
1325
  "builder.formBuilder": "Form builder",
1259
1326
  "builder.basicSettings": "Basic settings",
@@ -1272,6 +1339,10 @@ var BUILDER_DEFAULTS = {
1272
1339
  "builder.required": "Required",
1273
1340
  "builder.minimum": "Minimum",
1274
1341
  "builder.maximum": "Maximum",
1342
+ "builder.minimumLength": "Minimum length",
1343
+ "builder.maximumLength": "Maximum length",
1344
+ "builder.pattern": "Pattern",
1345
+ "builder.step": "Step",
1275
1346
  "builder.options": "Options",
1276
1347
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1277
1348
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1334,6 +1405,18 @@ var BUILDER_DEFAULTS = {
1334
1405
  "builder.fields.typeMultiSelect": "Multi-select",
1335
1406
  "builder.fields.typeCheckbox": "Checkbox",
1336
1407
  "builder.fields.typeRadio": "Radio",
1408
+ "builder.fieldCategory.text": "Text",
1409
+ "builder.fieldCategory.choice": "Choice",
1410
+ "builder.fieldCategory.number": "Number",
1411
+ "builder.fieldCategory.advanced": "Advanced",
1412
+ "builder.fieldTypeDescription.text": "A single-line text answer",
1413
+ "builder.fieldTypeDescription.textarea": "A long-form text answer",
1414
+ "builder.fieldTypeDescription.number": "A numeric answer",
1415
+ "builder.fieldTypeDescription.rating": "A rating scale answer",
1416
+ "builder.fieldTypeDescription.radio": "A single-choice answer",
1417
+ "builder.fieldTypeDescription.checkbox": "A yes/no answer",
1418
+ "builder.fieldTypeDescription.select": "A dropdown choice",
1419
+ "builder.fieldTypeDescription.multi-select": "Multiple choices",
1337
1420
  "builder.actions.addField": "Add question",
1338
1421
  "builder.fieldType.text": "Text",
1339
1422
  "builder.fieldType.textarea": "Textarea",
@@ -1348,12 +1431,6 @@ var BUILDER_DEFAULTS = {
1348
1431
  "builder.operator.contains": "contains",
1349
1432
  "builder.operator.not_empty": "is not empty"
1350
1433
  };
1351
- function interpolate(template, params) {
1352
- return template.replace(
1353
- /\{\{(\w+)\}\}/g,
1354
- (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
1355
- );
1356
- }
1357
1434
  function BuilderSectionGroup({ children }) {
1358
1435
  return children;
1359
1436
  }
@@ -1523,6 +1600,8 @@ function FormBuilder({
1523
1600
  createManualTranslationMetadata,
1524
1601
  readOnly = false,
1525
1602
  features,
1603
+ fieldEditorControls,
1604
+ fieldTypeOptions,
1526
1605
  components: componentOverrides,
1527
1606
  slots,
1528
1607
  sectionOrder,
@@ -1558,20 +1637,14 @@ function FormBuilder({
1558
1637
  const [isTranslating, setIsTranslating] = (0, import_react2.useState)(false);
1559
1638
  const [translationError, setTranslationError] = (0, import_react2.useState)(null);
1560
1639
  const [translationReport, setTranslationReport] = (0, import_react2.useState)();
1561
- const translate = (key, params = {}) => {
1562
- const translatorParams = {};
1563
- for (const [name, value] of Object.entries(params)) {
1564
- if (typeof value === "string" || typeof value === "number") translatorParams[name] = value;
1565
- }
1566
- const translated = translator?.translate(key, locale, translatorParams);
1567
- if (translated !== void 0 && translated !== key) return translated;
1568
- const alias = BUILDER_TRANSLATION_ALIASES[key];
1569
- if (alias !== void 0) {
1570
- const aliased = translator?.translate(alias, locale, translatorParams);
1571
- if (aliased !== void 0 && aliased !== alias) return aliased;
1572
- }
1573
- return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
1574
- };
1640
+ const translate = (key, params = {}) => resolveTranslation(
1641
+ key,
1642
+ BUILDER_TRANSLATION_ALIASES[key] === void 0 ? [] : [BUILDER_TRANSLATION_ALIASES[key]],
1643
+ translator,
1644
+ BUILDER_DEFAULTS,
1645
+ params,
1646
+ locale
1647
+ );
1575
1648
  const pagesEnabled = features?.pages ?? true;
1576
1649
  const localizationEnabled = features?.localization ?? true;
1577
1650
  const conditionsEnabled = features?.conditions ?? true;
@@ -2293,6 +2366,7 @@ function FormBuilder({
2293
2366
  }
2294
2367
  ) : null }),
2295
2368
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2369
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2296
2370
  const condition = field.displayCondition;
2297
2371
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2298
2372
  const availableSources = schema.fields.slice(0, index);
@@ -2308,6 +2382,8 @@ function FormBuilder({
2308
2382
  ...slots === void 0 ? {} : { slots },
2309
2383
  ...policy === void 0 ? {} : { policy },
2310
2384
  ...features === void 0 ? {} : { features },
2385
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2386
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2311
2387
  readOnly,
2312
2388
  actions,
2313
2389
  components
@@ -2369,7 +2445,7 @@ function FormBuilder({
2369
2445
  }
2370
2446
  ) }),
2371
2447
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2372
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2448
+ controls.title === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2373
2449
  TextInput,
2374
2450
  {
2375
2451
  id: `builder-field-${field.id}-title`,
@@ -2381,34 +2457,56 @@ function FormBuilder({
2381
2457
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2382
2458
  value: field.title,
2383
2459
  placeholder: translate("builder.questionTitlePlaceholder"),
2460
+ readOnly: controls.title === "readOnly",
2384
2461
  onChange: (value) => updateField(field.id, (current) => ({
2385
2462
  ...current,
2386
2463
  title: value.trim().length === 0 ? current.title : value
2387
2464
  }))
2388
2465
  }
2389
2466
  ) }),
2390
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2467
+ controls.typeSelect === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2391
2468
  Select,
2392
2469
  {
2393
2470
  id: `builder-field-${field.id}-type`,
2394
2471
  label: translate("builder.type"),
2395
2472
  value: field.type,
2473
+ disabled: controls.typeSelect === "readOnly",
2396
2474
  onChange: (value) => changeType(field.id, value),
2397
- options: FIELD_TYPES.filter(
2398
- (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2399
- ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) }))
2475
+ options: resolveFieldTypeSelectOptions(
2476
+ FIELD_TYPES.filter(
2477
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2478
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2479
+ fieldTypeOptions,
2480
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2481
+ )
2400
2482
  }
2401
2483
  ) }),
2402
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2484
+ controls.required === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2403
2485
  Checkbox,
2404
2486
  {
2405
2487
  className: builderClass("form-engine-builder__check"),
2406
2488
  checked: field.required === true,
2489
+ disabled: controls.required === "readOnly",
2407
2490
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2408
2491
  label: translate("builder.required")
2409
2492
  }
2410
2493
  )
2411
2494
  ] }),
2495
+ controls.description === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2496
+ TextArea,
2497
+ {
2498
+ id: `builder-field-${field.id}-description`,
2499
+ name: `fields.${field.id}.description`,
2500
+ label: translate("builder.description"),
2501
+ value: field.description ?? "",
2502
+ readOnly: controls.description === "readOnly",
2503
+ onChange: (value) => updateField(field.id, (current) => {
2504
+ if (value.length > 0) return { ...current, description: value };
2505
+ const { description: _description, ...remaining } = current;
2506
+ return remaining;
2507
+ })
2508
+ }
2509
+ ) }),
2412
2510
  !pagesEnabled || schema.pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2413
2511
  Select,
2414
2512
  {
@@ -2444,12 +2542,13 @@ function FormBuilder({
2444
2542
  })
2445
2543
  }
2446
2544
  ) }),
2447
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2545
+ controls.description === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2448
2546
  TextInput,
2449
2547
  {
2450
2548
  id: `builder-field-${field.id}-${editingLocale}-description`,
2451
2549
  label: translate("builder.pageDescription"),
2452
2550
  value: field.translations?.[editingLocale]?.description ?? "",
2551
+ readOnly: controls.description === "readOnly",
2453
2552
  onChange: (value) => updateManualTranslation({
2454
2553
  locale: editingLocale,
2455
2554
  kind: "field",
@@ -2484,7 +2583,7 @@ function FormBuilder({
2484
2583
  }
2485
2584
  ) }, option.id)) : null
2486
2585
  ] }),
2487
- field.type === "rating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2586
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2488
2587
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2489
2588
  TextInput,
2490
2589
  {
@@ -2492,6 +2591,7 @@ function FormBuilder({
2492
2591
  label: translate("builder.minimum"),
2493
2592
  type: "number",
2494
2593
  value: String(field.min ?? 1),
2594
+ readOnly: controls.ratingBounds === "readOnly",
2495
2595
  onChange: (value) => {
2496
2596
  const min = Number(value);
2497
2597
  if (!Number.isInteger(min)) return;
@@ -2509,6 +2609,7 @@ function FormBuilder({
2509
2609
  label: translate("builder.maximum"),
2510
2610
  type: "number",
2511
2611
  value: String(field.max ?? 5),
2612
+ readOnly: controls.ratingBounds === "readOnly",
2512
2613
  onChange: (value) => {
2513
2614
  const max = Number(value);
2514
2615
  if (!Number.isInteger(max)) return;
@@ -2520,7 +2621,73 @@ function FormBuilder({
2520
2621
  }
2521
2622
  ) })
2522
2623
  ] }) : null,
2523
- "options" in field ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2624
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2625
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2626
+ TextInput,
2627
+ {
2628
+ id: `builder-field-${field.id}-min-length`,
2629
+ label: translate("builder.minimumLength"),
2630
+ type: "number",
2631
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2632
+ readOnly: controls.textLimits === "readOnly",
2633
+ onChange: (value) => updateField(field.id, (current) => {
2634
+ if (current.type !== "text" && current.type !== "textarea") return current;
2635
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2636
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2637
+ })
2638
+ }
2639
+ ) }),
2640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2641
+ TextInput,
2642
+ {
2643
+ id: `builder-field-${field.id}-max-length`,
2644
+ label: translate("builder.maximumLength"),
2645
+ type: "number",
2646
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2647
+ readOnly: controls.textLimits === "readOnly",
2648
+ onChange: (value) => updateField(field.id, (current) => {
2649
+ if (current.type !== "text" && current.type !== "textarea") return current;
2650
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2651
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2652
+ })
2653
+ }
2654
+ ) }),
2655
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2656
+ TextInput,
2657
+ {
2658
+ id: `builder-field-${field.id}-pattern`,
2659
+ label: translate("builder.pattern"),
2660
+ value: field.pattern ?? "",
2661
+ readOnly: controls.textLimits === "readOnly",
2662
+ onChange: (value) => updateField(field.id, (current) => {
2663
+ if (current.type !== "text" && current.type !== "textarea") return current;
2664
+ return value.length === 0 ? current : { ...current, pattern: value };
2665
+ })
2666
+ }
2667
+ ) })
2668
+ ] }) : null,
2669
+ field.type === "number" && controls.numberLimits !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__grid"), children: ["min", "max", "step"].map((property) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2670
+ TextInput,
2671
+ {
2672
+ id: `builder-field-${field.id}-${property}`,
2673
+ label: translate(
2674
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2675
+ ),
2676
+ type: "number",
2677
+ value: field[property] === void 0 ? "" : String(field[property]),
2678
+ readOnly: controls.numberLimits === "readOnly",
2679
+ onChange: (value) => updateField(field.id, (current) => {
2680
+ if (current.type !== "number") return current;
2681
+ if (value.trim().length === 0) {
2682
+ const { [property]: _removed, ...remaining } = current;
2683
+ return remaining;
2684
+ }
2685
+ const parsed = Number(value);
2686
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2687
+ })
2688
+ }
2689
+ ) }, property)) }) : null,
2690
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2524
2691
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: translate("builder.options") }),
2525
2692
  field.options.map(
2526
2693
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2531,6 +2698,7 @@ function FormBuilder({
2531
2698
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2532
2699
  value: option.label,
2533
2700
  placeholder: translate("builder.optionLabelPlaceholder"),
2701
+ readOnly: controls.options === "readOnly",
2534
2702
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2535
2703
  }
2536
2704
  ),
@@ -2540,7 +2708,7 @@ function FormBuilder({
2540
2708
  {
2541
2709
  actionType: "moveUp",
2542
2710
  title: translate("builder.moveUp", { title: option.label }),
2543
- disabled: optionIndex === 0,
2711
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2544
2712
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2545
2713
  }
2546
2714
  ),
@@ -2549,7 +2717,7 @@ function FormBuilder({
2549
2717
  {
2550
2718
  actionType: "moveDown",
2551
2719
  title: translate("builder.moveDown", { title: option.label }),
2552
- disabled: optionIndex === field.options.length - 1,
2720
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2553
2721
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2554
2722
  }
2555
2723
  ),
@@ -2557,7 +2725,7 @@ function FormBuilder({
2557
2725
  IconButton,
2558
2726
  {
2559
2727
  actionType: "delete",
2560
- disabled: field.options.length === 1,
2728
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2561
2729
  onClick: () => removeOption(field.id, option.id),
2562
2730
  title: translate("builder.remove")
2563
2731
  }
@@ -2575,7 +2743,7 @@ function FormBuilder({
2575
2743
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2576
2744
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2577
2745
  onRemove: () => removeOption(field.id, option.id),
2578
- readOnly,
2746
+ readOnly: readOnly || controls.options === "readOnly",
2579
2747
  actions,
2580
2748
  components
2581
2749
  }
@@ -2589,7 +2757,7 @@ function FormBuilder({
2589
2757
  index: optionIndex,
2590
2758
  currentLocale: editingLocale,
2591
2759
  translate,
2592
- readOnly,
2760
+ readOnly: readOnly || controls.options === "readOnly",
2593
2761
  actions,
2594
2762
  components
2595
2763
  },
@@ -2601,13 +2769,13 @@ function FormBuilder({
2601
2769
  {
2602
2770
  action: "addOption",
2603
2771
  targetId: field.id,
2604
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2772
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2605
2773
  onClick: () => addOption(field.id),
2606
2774
  children: translate("builder.addOption")
2607
2775
  }
2608
2776
  )
2609
2777
  ] }) : null,
2610
- conditionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2778
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2611
2779
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2612
2780
  Select,
2613
2781
  {
@@ -2852,7 +3020,7 @@ function FormProvider({
2852
3020
  [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2853
3021
  );
2854
3022
  const translate = (0, import_react3.useCallback)(
2855
- (key, params) => translator.translate(key, locale, params),
3023
+ (key, params) => translator.translate(key, locale, params) ?? key,
2856
3024
  [locale, translator]
2857
3025
  );
2858
3026
  const contextValue = (0, import_react3.useMemo)(
@@ -3963,7 +4131,11 @@ function FormRenderer(props) {
3963
4131
  FormSubmissionError,
3964
4132
  createLocalStorageSubmissionAttemptStore,
3965
4133
  createLocalStorageSubmissionReceiptStore,
4134
+ isTranslationUnresolved,
4135
+ resolveFieldEditorControls,
4136
+ resolveFieldTypeSelectOptions,
3966
4137
  resolveInitialFieldType,
4138
+ resolveTranslation,
3967
4139
  submissionReceiptQueryKey,
3968
4140
  useField,
3969
4141
  useForm,
package/dist/index.d.cts CHANGED
@@ -187,19 +187,45 @@ interface BuilderSelectOption<T extends string = string> {
187
187
  readonly icon?: ReactNode;
188
188
  readonly description?: string;
189
189
  readonly disabled?: boolean;
190
+ readonly group?: string;
191
+ readonly groupLabel?: string;
190
192
  readonly kind?: string;
191
193
  readonly metadata?: Readonly<Record<string, unknown>>;
192
194
  }
193
- interface SelectComponentProps extends InputComponentProps {
194
- readonly options: readonly (string | BuilderSelectOption)[];
195
- readonly renderOption?: (option: BuilderSelectOption) => ReactNode;
196
- readonly renderValue?: (option: BuilderSelectOption | undefined) => ReactNode;
195
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
196
+ interface FieldEditorControlsConfig {
197
+ readonly title?: FieldPropertyControlMode;
198
+ readonly description?: FieldPropertyControlMode;
199
+ readonly required?: FieldPropertyControlMode;
200
+ readonly typeSelect?: FieldPropertyControlMode;
201
+ readonly options?: FieldPropertyControlMode;
202
+ readonly displayConditions?: FieldPropertyControlMode;
203
+ readonly textLimits?: FieldPropertyControlMode;
204
+ readonly ratingBounds?: FieldPropertyControlMode;
205
+ readonly numberLimits?: FieldPropertyControlMode;
206
+ }
207
+ interface FieldTypeSelectOptionsContext {
208
+ readonly currentType: QuestionType;
209
+ readonly allowedTypes: readonly QuestionType[];
197
210
  }
198
- interface BuilderSelectProps extends InputComponentProps {
199
- readonly options: readonly BuilderSelectOption[];
200
- readonly renderOption?: (option: BuilderSelectOption) => ReactNode;
201
- readonly renderValue?: (option: BuilderSelectOption | undefined) => ReactNode;
211
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
212
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
213
+ interface FieldTypeSelectOptionsConfig {
214
+ /** Transform the generated choices. The returned array is used as-is. */
215
+ readonly transform?: FieldTypeSelectOptionsTransformer;
216
+ /** Sort generated choices after transform. The source array is never mutated. */
217
+ readonly sort?: FieldTypeSelectOptionsSorter;
218
+ /** Optional explicit order applied before `sort`. */
219
+ readonly order?: readonly QuestionType[];
220
+ }
221
+ interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
222
+ readonly value: T;
223
+ readonly onChange: (value: T) => void;
224
+ readonly options: readonly (T | BuilderSelectOption<T>)[];
225
+ readonly renderOption?: (option: BuilderSelectOption<T>) => ReactNode;
226
+ readonly renderValue?: (option: BuilderSelectOption<T> | undefined) => ReactNode;
202
227
  }
228
+ type BuilderSelectProps<T extends string = string> = SelectComponentProps<T>;
203
229
  interface BuilderCheckboxProps extends ComponentBaseProps {
204
230
  readonly name?: string;
205
231
  readonly required?: boolean;
@@ -278,14 +304,25 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
304
  readonly localization?: boolean;
279
305
  readonly conditions?: boolean;
280
306
  };
307
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
308
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
309
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
310
  }
283
311
  interface FieldTypeSelectSlotProps {
312
+ readonly id?: string;
313
+ readonly name?: string;
314
+ readonly label?: string;
284
315
  readonly currentType: QuestionType;
285
316
  readonly allowedTypes: readonly QuestionType[];
317
+ readonly options: readonly BuilderSelectOption<QuestionType>[];
286
318
  readonly onChangeType: (nextType: QuestionType) => void;
287
319
  readonly disabled?: boolean;
288
320
  readonly readOnly?: boolean;
321
+ readonly required?: boolean;
322
+ readonly error?: boolean;
323
+ readonly helperText?: string;
324
+ readonly "aria-describedby"?: string;
325
+ readonly "aria-labelledby"?: string;
289
326
  readonly renderIcon?: (type: QuestionType) => ReactNode;
290
327
  }
291
328
  interface FieldEditorHeaderSlotProps {
@@ -515,6 +552,8 @@ interface SubmissionProtectionProps {
515
552
  }
516
553
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
517
554
 
555
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
556
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
518
557
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
519
558
  interface FormBuilderFeatures {
520
559
  readonly pages?: boolean;
@@ -538,13 +577,15 @@ interface FormBuilderProps {
538
577
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
539
578
  readonly readOnly?: boolean;
540
579
  readonly features?: FormBuilderFeatures;
580
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
581
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
541
582
  readonly components?: FormBuilderComponents;
542
583
  readonly slots?: FormBuilderSlots;
543
584
  readonly sectionOrder?: readonly FormBuilderSectionName[];
544
585
  readonly disableDefaultStyles?: boolean;
545
586
  readonly unstyled?: boolean;
546
587
  }
547
- declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
588
+ declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, fieldEditorControls, fieldTypeOptions, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
548
589
 
549
590
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
550
591
  interface FormContextValue {
@@ -601,6 +642,8 @@ declare const BUILDER_TRANSLATION_KEYS: {
601
642
  type BuilderTranslationKey = (typeof BUILDER_TRANSLATION_KEYS)[keyof typeof BUILDER_TRANSLATION_KEYS];
602
643
  /** Legacy keys are checked when an older catalog does not contain a canonical key. */
603
644
  declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
645
+ declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
646
+ declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
604
647
 
605
648
  interface FieldComponentProps {
606
649
  readonly field: FormField;
@@ -648,4 +691,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
648
691
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
649
692
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
650
693
 
651
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorHeaderSlotProps, type FieldState, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
694
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.d.ts CHANGED
@@ -187,19 +187,45 @@ interface BuilderSelectOption<T extends string = string> {
187
187
  readonly icon?: ReactNode;
188
188
  readonly description?: string;
189
189
  readonly disabled?: boolean;
190
+ readonly group?: string;
191
+ readonly groupLabel?: string;
190
192
  readonly kind?: string;
191
193
  readonly metadata?: Readonly<Record<string, unknown>>;
192
194
  }
193
- interface SelectComponentProps extends InputComponentProps {
194
- readonly options: readonly (string | BuilderSelectOption)[];
195
- readonly renderOption?: (option: BuilderSelectOption) => ReactNode;
196
- readonly renderValue?: (option: BuilderSelectOption | undefined) => ReactNode;
195
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
196
+ interface FieldEditorControlsConfig {
197
+ readonly title?: FieldPropertyControlMode;
198
+ readonly description?: FieldPropertyControlMode;
199
+ readonly required?: FieldPropertyControlMode;
200
+ readonly typeSelect?: FieldPropertyControlMode;
201
+ readonly options?: FieldPropertyControlMode;
202
+ readonly displayConditions?: FieldPropertyControlMode;
203
+ readonly textLimits?: FieldPropertyControlMode;
204
+ readonly ratingBounds?: FieldPropertyControlMode;
205
+ readonly numberLimits?: FieldPropertyControlMode;
206
+ }
207
+ interface FieldTypeSelectOptionsContext {
208
+ readonly currentType: QuestionType;
209
+ readonly allowedTypes: readonly QuestionType[];
197
210
  }
198
- interface BuilderSelectProps extends InputComponentProps {
199
- readonly options: readonly BuilderSelectOption[];
200
- readonly renderOption?: (option: BuilderSelectOption) => ReactNode;
201
- readonly renderValue?: (option: BuilderSelectOption | undefined) => ReactNode;
211
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
212
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
213
+ interface FieldTypeSelectOptionsConfig {
214
+ /** Transform the generated choices. The returned array is used as-is. */
215
+ readonly transform?: FieldTypeSelectOptionsTransformer;
216
+ /** Sort generated choices after transform. The source array is never mutated. */
217
+ readonly sort?: FieldTypeSelectOptionsSorter;
218
+ /** Optional explicit order applied before `sort`. */
219
+ readonly order?: readonly QuestionType[];
220
+ }
221
+ interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
222
+ readonly value: T;
223
+ readonly onChange: (value: T) => void;
224
+ readonly options: readonly (T | BuilderSelectOption<T>)[];
225
+ readonly renderOption?: (option: BuilderSelectOption<T>) => ReactNode;
226
+ readonly renderValue?: (option: BuilderSelectOption<T> | undefined) => ReactNode;
202
227
  }
228
+ type BuilderSelectProps<T extends string = string> = SelectComponentProps<T>;
203
229
  interface BuilderCheckboxProps extends ComponentBaseProps {
204
230
  readonly name?: string;
205
231
  readonly required?: boolean;
@@ -278,14 +304,25 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
304
  readonly localization?: boolean;
279
305
  readonly conditions?: boolean;
280
306
  };
307
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
308
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
309
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
310
  }
283
311
  interface FieldTypeSelectSlotProps {
312
+ readonly id?: string;
313
+ readonly name?: string;
314
+ readonly label?: string;
284
315
  readonly currentType: QuestionType;
285
316
  readonly allowedTypes: readonly QuestionType[];
317
+ readonly options: readonly BuilderSelectOption<QuestionType>[];
286
318
  readonly onChangeType: (nextType: QuestionType) => void;
287
319
  readonly disabled?: boolean;
288
320
  readonly readOnly?: boolean;
321
+ readonly required?: boolean;
322
+ readonly error?: boolean;
323
+ readonly helperText?: string;
324
+ readonly "aria-describedby"?: string;
325
+ readonly "aria-labelledby"?: string;
289
326
  readonly renderIcon?: (type: QuestionType) => ReactNode;
290
327
  }
291
328
  interface FieldEditorHeaderSlotProps {
@@ -515,6 +552,8 @@ interface SubmissionProtectionProps {
515
552
  }
516
553
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
517
554
 
555
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
556
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
518
557
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
519
558
  interface FormBuilderFeatures {
520
559
  readonly pages?: boolean;
@@ -538,13 +577,15 @@ interface FormBuilderProps {
538
577
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
539
578
  readonly readOnly?: boolean;
540
579
  readonly features?: FormBuilderFeatures;
580
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
581
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
541
582
  readonly components?: FormBuilderComponents;
542
583
  readonly slots?: FormBuilderSlots;
543
584
  readonly sectionOrder?: readonly FormBuilderSectionName[];
544
585
  readonly disableDefaultStyles?: boolean;
545
586
  readonly unstyled?: boolean;
546
587
  }
547
- declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
588
+ declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, fieldEditorControls, fieldTypeOptions, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
548
589
 
549
590
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
550
591
  interface FormContextValue {
@@ -601,6 +642,8 @@ declare const BUILDER_TRANSLATION_KEYS: {
601
642
  type BuilderTranslationKey = (typeof BUILDER_TRANSLATION_KEYS)[keyof typeof BUILDER_TRANSLATION_KEYS];
602
643
  /** Legacy keys are checked when an older catalog does not contain a canonical key. */
603
644
  declare const BUILDER_TRANSLATION_ALIASES: Readonly<Record<string, string>>;
645
+ declare function isTranslationUnresolved(result: unknown, key: string, aliases?: readonly string[]): boolean;
646
+ declare function resolveTranslation(key: string, aliases: readonly string[], adapter?: TranslationAdapter, defaultCatalog?: Readonly<Record<string, string>>, params?: Readonly<Record<string, unknown>>, locale?: string): string;
604
647
 
605
648
  interface FieldComponentProps {
606
649
  readonly field: FormField;
@@ -648,4 +691,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
648
691
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
649
692
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
650
693
 
651
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorHeaderSlotProps, type FieldState, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, resolveInitialFieldType, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
694
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.js CHANGED
@@ -770,6 +770,41 @@ var BUILDER_TRANSLATION_ALIASES = {
770
770
  "builder.fields.typeSelect": "builder.fieldType.select",
771
771
  "builder.fields.typeMultiSelect": "builder.fieldType.multi-select"
772
772
  };
773
+ function isTranslationUnresolved(result, key, aliases = []) {
774
+ if (result === void 0 || result === null || result === "") return true;
775
+ if (typeof result !== "string") return false;
776
+ if (result === key || aliases.includes(result)) return true;
777
+ return result.endsWith(key) || aliases.some((alias) => result.endsWith(alias));
778
+ }
779
+ function isResolvedAdapterResult(result, key, aliases) {
780
+ return typeof result === "string" && result.startsWith("translated:") ? true : !isTranslationUnresolved(result, key, aliases);
781
+ }
782
+ function formatTemplate(template, params) {
783
+ return template.replace(
784
+ /\{\{(\w+)\}\}/g,
785
+ (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
786
+ );
787
+ }
788
+ function resolveTranslation(key, aliases, adapter, defaultCatalog, params = {}, locale = "") {
789
+ const adapterParams = {};
790
+ for (const [name, value] of Object.entries(params)) {
791
+ if (typeof value === "string" || typeof value === "number") adapterParams[name] = value;
792
+ }
793
+ const translate = (candidate) => adapter?.translate(candidate, locale, adapterParams);
794
+ const translated = translate(key);
795
+ if (isResolvedAdapterResult(translated, key, aliases)) return translated;
796
+ for (const alias of aliases) {
797
+ const aliased = translate(alias);
798
+ if (isResolvedAdapterResult(aliased, alias, aliases)) return aliased;
799
+ }
800
+ const catalogValue = defaultCatalog?.[key];
801
+ if (catalogValue !== void 0) return formatTemplate(catalogValue, params);
802
+ for (const alias of aliases) {
803
+ const aliasValue = defaultCatalog?.[alias];
804
+ if (aliasValue !== void 0) return formatTemplate(aliasValue, params);
805
+ }
806
+ return key;
807
+ }
773
808
 
774
809
  // src/builder.tsx
775
810
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1222,6 +1257,34 @@ var FIELD_TYPES = [
1222
1257
  "checkbox",
1223
1258
  "radio"
1224
1259
  ];
1260
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1261
+ function resolveFieldEditorControls(config = {}) {
1262
+ return {
1263
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1264
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1265
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1266
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1267
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1268
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1269
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1270
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1271
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1272
+ };
1273
+ }
1274
+ function resolveFieldTypeSelectOptions(options, config, context) {
1275
+ let resolved = [...options];
1276
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1277
+ if (config?.order !== void 0) {
1278
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1279
+ resolved.sort((left, right) => {
1280
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1281
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1282
+ return leftRank - rightRank;
1283
+ });
1284
+ }
1285
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1286
+ return resolved;
1287
+ }
1225
1288
  var BUILDER_DEFAULTS = {
1226
1289
  "builder.formBuilder": "Form builder",
1227
1290
  "builder.basicSettings": "Basic settings",
@@ -1240,6 +1303,10 @@ var BUILDER_DEFAULTS = {
1240
1303
  "builder.required": "Required",
1241
1304
  "builder.minimum": "Minimum",
1242
1305
  "builder.maximum": "Maximum",
1306
+ "builder.minimumLength": "Minimum length",
1307
+ "builder.maximumLength": "Maximum length",
1308
+ "builder.pattern": "Pattern",
1309
+ "builder.step": "Step",
1243
1310
  "builder.options": "Options",
1244
1311
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1245
1312
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1302,6 +1369,18 @@ var BUILDER_DEFAULTS = {
1302
1369
  "builder.fields.typeMultiSelect": "Multi-select",
1303
1370
  "builder.fields.typeCheckbox": "Checkbox",
1304
1371
  "builder.fields.typeRadio": "Radio",
1372
+ "builder.fieldCategory.text": "Text",
1373
+ "builder.fieldCategory.choice": "Choice",
1374
+ "builder.fieldCategory.number": "Number",
1375
+ "builder.fieldCategory.advanced": "Advanced",
1376
+ "builder.fieldTypeDescription.text": "A single-line text answer",
1377
+ "builder.fieldTypeDescription.textarea": "A long-form text answer",
1378
+ "builder.fieldTypeDescription.number": "A numeric answer",
1379
+ "builder.fieldTypeDescription.rating": "A rating scale answer",
1380
+ "builder.fieldTypeDescription.radio": "A single-choice answer",
1381
+ "builder.fieldTypeDescription.checkbox": "A yes/no answer",
1382
+ "builder.fieldTypeDescription.select": "A dropdown choice",
1383
+ "builder.fieldTypeDescription.multi-select": "Multiple choices",
1305
1384
  "builder.actions.addField": "Add question",
1306
1385
  "builder.fieldType.text": "Text",
1307
1386
  "builder.fieldType.textarea": "Textarea",
@@ -1316,12 +1395,6 @@ var BUILDER_DEFAULTS = {
1316
1395
  "builder.operator.contains": "contains",
1317
1396
  "builder.operator.not_empty": "is not empty"
1318
1397
  };
1319
- function interpolate(template, params) {
1320
- return template.replace(
1321
- /\{\{(\w+)\}\}/g,
1322
- (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
1323
- );
1324
- }
1325
1398
  function BuilderSectionGroup({ children }) {
1326
1399
  return children;
1327
1400
  }
@@ -1491,6 +1564,8 @@ function FormBuilder({
1491
1564
  createManualTranslationMetadata,
1492
1565
  readOnly = false,
1493
1566
  features,
1567
+ fieldEditorControls,
1568
+ fieldTypeOptions,
1494
1569
  components: componentOverrides,
1495
1570
  slots,
1496
1571
  sectionOrder,
@@ -1526,20 +1601,14 @@ function FormBuilder({
1526
1601
  const [isTranslating, setIsTranslating] = useState(false);
1527
1602
  const [translationError, setTranslationError] = useState(null);
1528
1603
  const [translationReport, setTranslationReport] = useState();
1529
- const translate = (key, params = {}) => {
1530
- const translatorParams = {};
1531
- for (const [name, value] of Object.entries(params)) {
1532
- if (typeof value === "string" || typeof value === "number") translatorParams[name] = value;
1533
- }
1534
- const translated = translator?.translate(key, locale, translatorParams);
1535
- if (translated !== void 0 && translated !== key) return translated;
1536
- const alias = BUILDER_TRANSLATION_ALIASES[key];
1537
- if (alias !== void 0) {
1538
- const aliased = translator?.translate(alias, locale, translatorParams);
1539
- if (aliased !== void 0 && aliased !== alias) return aliased;
1540
- }
1541
- return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
1542
- };
1604
+ const translate = (key, params = {}) => resolveTranslation(
1605
+ key,
1606
+ BUILDER_TRANSLATION_ALIASES[key] === void 0 ? [] : [BUILDER_TRANSLATION_ALIASES[key]],
1607
+ translator,
1608
+ BUILDER_DEFAULTS,
1609
+ params,
1610
+ locale
1611
+ );
1543
1612
  const pagesEnabled = features?.pages ?? true;
1544
1613
  const localizationEnabled = features?.localization ?? true;
1545
1614
  const conditionsEnabled = features?.conditions ?? true;
@@ -2261,6 +2330,7 @@ function FormBuilder({
2261
2330
  }
2262
2331
  ) : null }),
2263
2332
  /* @__PURE__ */ jsx(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2333
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2264
2334
  const condition = field.displayCondition;
2265
2335
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2266
2336
  const availableSources = schema.fields.slice(0, index);
@@ -2276,6 +2346,8 @@ function FormBuilder({
2276
2346
  ...slots === void 0 ? {} : { slots },
2277
2347
  ...policy === void 0 ? {} : { policy },
2278
2348
  ...features === void 0 ? {} : { features },
2349
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2350
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2279
2351
  readOnly,
2280
2352
  actions,
2281
2353
  components
@@ -2337,7 +2409,7 @@ function FormBuilder({
2337
2409
  }
2338
2410
  ) }),
2339
2411
  /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2340
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2412
+ controls.title === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2341
2413
  TextInput,
2342
2414
  {
2343
2415
  id: `builder-field-${field.id}-title`,
@@ -2349,34 +2421,56 @@ function FormBuilder({
2349
2421
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2350
2422
  value: field.title,
2351
2423
  placeholder: translate("builder.questionTitlePlaceholder"),
2424
+ readOnly: controls.title === "readOnly",
2352
2425
  onChange: (value) => updateField(field.id, (current) => ({
2353
2426
  ...current,
2354
2427
  title: value.trim().length === 0 ? current.title : value
2355
2428
  }))
2356
2429
  }
2357
2430
  ) }),
2358
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2431
+ controls.typeSelect === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2359
2432
  Select,
2360
2433
  {
2361
2434
  id: `builder-field-${field.id}-type`,
2362
2435
  label: translate("builder.type"),
2363
2436
  value: field.type,
2437
+ disabled: controls.typeSelect === "readOnly",
2364
2438
  onChange: (value) => changeType(field.id, value),
2365
- options: FIELD_TYPES.filter(
2366
- (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2367
- ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) }))
2439
+ options: resolveFieldTypeSelectOptions(
2440
+ FIELD_TYPES.filter(
2441
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2442
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2443
+ fieldTypeOptions,
2444
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2445
+ )
2368
2446
  }
2369
2447
  ) }),
2370
- /* @__PURE__ */ jsx(
2448
+ controls.required === "hidden" ? null : /* @__PURE__ */ jsx(
2371
2449
  Checkbox,
2372
2450
  {
2373
2451
  className: builderClass("form-engine-builder__check"),
2374
2452
  checked: field.required === true,
2453
+ disabled: controls.required === "readOnly",
2375
2454
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2376
2455
  label: translate("builder.required")
2377
2456
  }
2378
2457
  )
2379
2458
  ] }),
2459
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2460
+ TextArea,
2461
+ {
2462
+ id: `builder-field-${field.id}-description`,
2463
+ name: `fields.${field.id}.description`,
2464
+ label: translate("builder.description"),
2465
+ value: field.description ?? "",
2466
+ readOnly: controls.description === "readOnly",
2467
+ onChange: (value) => updateField(field.id, (current) => {
2468
+ if (value.length > 0) return { ...current, description: value };
2469
+ const { description: _description, ...remaining } = current;
2470
+ return remaining;
2471
+ })
2472
+ }
2473
+ ) }),
2380
2474
  !pagesEnabled || schema.pages === void 0 ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2381
2475
  Select,
2382
2476
  {
@@ -2412,12 +2506,13 @@ function FormBuilder({
2412
2506
  })
2413
2507
  }
2414
2508
  ) }),
2415
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2509
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2416
2510
  TextInput,
2417
2511
  {
2418
2512
  id: `builder-field-${field.id}-${editingLocale}-description`,
2419
2513
  label: translate("builder.pageDescription"),
2420
2514
  value: field.translations?.[editingLocale]?.description ?? "",
2515
+ readOnly: controls.description === "readOnly",
2421
2516
  onChange: (value) => updateManualTranslation({
2422
2517
  locale: editingLocale,
2423
2518
  kind: "field",
@@ -2452,7 +2547,7 @@ function FormBuilder({
2452
2547
  }
2453
2548
  ) }, option.id)) : null
2454
2549
  ] }),
2455
- field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2550
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2456
2551
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2457
2552
  TextInput,
2458
2553
  {
@@ -2460,6 +2555,7 @@ function FormBuilder({
2460
2555
  label: translate("builder.minimum"),
2461
2556
  type: "number",
2462
2557
  value: String(field.min ?? 1),
2558
+ readOnly: controls.ratingBounds === "readOnly",
2463
2559
  onChange: (value) => {
2464
2560
  const min = Number(value);
2465
2561
  if (!Number.isInteger(min)) return;
@@ -2477,6 +2573,7 @@ function FormBuilder({
2477
2573
  label: translate("builder.maximum"),
2478
2574
  type: "number",
2479
2575
  value: String(field.max ?? 5),
2576
+ readOnly: controls.ratingBounds === "readOnly",
2480
2577
  onChange: (value) => {
2481
2578
  const max = Number(value);
2482
2579
  if (!Number.isInteger(max)) return;
@@ -2488,7 +2585,73 @@ function FormBuilder({
2488
2585
  }
2489
2586
  ) })
2490
2587
  ] }) : null,
2491
- "options" in field ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2588
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2589
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2590
+ TextInput,
2591
+ {
2592
+ id: `builder-field-${field.id}-min-length`,
2593
+ label: translate("builder.minimumLength"),
2594
+ type: "number",
2595
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2596
+ readOnly: controls.textLimits === "readOnly",
2597
+ onChange: (value) => updateField(field.id, (current) => {
2598
+ if (current.type !== "text" && current.type !== "textarea") return current;
2599
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2600
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2601
+ })
2602
+ }
2603
+ ) }),
2604
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2605
+ TextInput,
2606
+ {
2607
+ id: `builder-field-${field.id}-max-length`,
2608
+ label: translate("builder.maximumLength"),
2609
+ type: "number",
2610
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2611
+ readOnly: controls.textLimits === "readOnly",
2612
+ onChange: (value) => updateField(field.id, (current) => {
2613
+ if (current.type !== "text" && current.type !== "textarea") return current;
2614
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2615
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2616
+ })
2617
+ }
2618
+ ) }),
2619
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2620
+ TextInput,
2621
+ {
2622
+ id: `builder-field-${field.id}-pattern`,
2623
+ label: translate("builder.pattern"),
2624
+ value: field.pattern ?? "",
2625
+ readOnly: controls.textLimits === "readOnly",
2626
+ onChange: (value) => updateField(field.id, (current) => {
2627
+ if (current.type !== "text" && current.type !== "textarea") return current;
2628
+ return value.length === 0 ? current : { ...current, pattern: value };
2629
+ })
2630
+ }
2631
+ ) })
2632
+ ] }) : null,
2633
+ field.type === "number" && controls.numberLimits !== "hidden" ? /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__grid"), children: ["min", "max", "step"].map((property) => /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2634
+ TextInput,
2635
+ {
2636
+ id: `builder-field-${field.id}-${property}`,
2637
+ label: translate(
2638
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2639
+ ),
2640
+ type: "number",
2641
+ value: field[property] === void 0 ? "" : String(field[property]),
2642
+ readOnly: controls.numberLimits === "readOnly",
2643
+ onChange: (value) => updateField(field.id, (current) => {
2644
+ if (current.type !== "number") return current;
2645
+ if (value.trim().length === 0) {
2646
+ const { [property]: _removed, ...remaining } = current;
2647
+ return remaining;
2648
+ }
2649
+ const parsed = Number(value);
2650
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2651
+ })
2652
+ }
2653
+ ) }, property)) }) : null,
2654
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2492
2655
  /* @__PURE__ */ jsx("strong", { children: translate("builder.options") }),
2493
2656
  field.options.map(
2494
2657
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2499,6 +2662,7 @@ function FormBuilder({
2499
2662
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2500
2663
  value: option.label,
2501
2664
  placeholder: translate("builder.optionLabelPlaceholder"),
2665
+ readOnly: controls.options === "readOnly",
2502
2666
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2503
2667
  }
2504
2668
  ),
@@ -2508,7 +2672,7 @@ function FormBuilder({
2508
2672
  {
2509
2673
  actionType: "moveUp",
2510
2674
  title: translate("builder.moveUp", { title: option.label }),
2511
- disabled: optionIndex === 0,
2675
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2512
2676
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2513
2677
  }
2514
2678
  ),
@@ -2517,7 +2681,7 @@ function FormBuilder({
2517
2681
  {
2518
2682
  actionType: "moveDown",
2519
2683
  title: translate("builder.moveDown", { title: option.label }),
2520
- disabled: optionIndex === field.options.length - 1,
2684
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2521
2685
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2522
2686
  }
2523
2687
  ),
@@ -2525,7 +2689,7 @@ function FormBuilder({
2525
2689
  IconButton,
2526
2690
  {
2527
2691
  actionType: "delete",
2528
- disabled: field.options.length === 1,
2692
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2529
2693
  onClick: () => removeOption(field.id, option.id),
2530
2694
  title: translate("builder.remove")
2531
2695
  }
@@ -2543,7 +2707,7 @@ function FormBuilder({
2543
2707
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2544
2708
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2545
2709
  onRemove: () => removeOption(field.id, option.id),
2546
- readOnly,
2710
+ readOnly: readOnly || controls.options === "readOnly",
2547
2711
  actions,
2548
2712
  components
2549
2713
  }
@@ -2557,7 +2721,7 @@ function FormBuilder({
2557
2721
  index: optionIndex,
2558
2722
  currentLocale: editingLocale,
2559
2723
  translate,
2560
- readOnly,
2724
+ readOnly: readOnly || controls.options === "readOnly",
2561
2725
  actions,
2562
2726
  components
2563
2727
  },
@@ -2569,13 +2733,13 @@ function FormBuilder({
2569
2733
  {
2570
2734
  action: "addOption",
2571
2735
  targetId: field.id,
2572
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2736
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2573
2737
  onClick: () => addOption(field.id),
2574
2738
  children: translate("builder.addOption")
2575
2739
  }
2576
2740
  )
2577
2741
  ] }) : null,
2578
- conditionsEnabled ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2742
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2579
2743
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2580
2744
  Select,
2581
2745
  {
@@ -2828,7 +2992,7 @@ function FormProvider({
2828
2992
  [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2829
2993
  );
2830
2994
  const translate = useCallback2(
2831
- (key, params) => translator.translate(key, locale, params),
2995
+ (key, params) => translator.translate(key, locale, params) ?? key,
2832
2996
  [locale, translator]
2833
2997
  );
2834
2998
  const contextValue = useMemo2(
@@ -3949,7 +4113,11 @@ export {
3949
4113
  FormSubmissionError,
3950
4114
  createLocalStorageSubmissionAttemptStore,
3951
4115
  createLocalStorageSubmissionReceiptStore,
4116
+ isTranslationUnresolved,
4117
+ resolveFieldEditorControls,
4118
+ resolveFieldTypeSelectOptions,
3952
4119
  resolveInitialFieldType,
4120
+ resolveTranslation,
3953
4121
  submissionReceiptQueryKey,
3954
4122
  useField,
3955
4123
  useForm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "3.2.0",
3
+ "version": "4.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "3.2.0",
46
- "@form-engine-ts/privacy": "3.2.0"
45
+ "@form-engine-ts/core": "4.1.0",
46
+ "@form-engine-ts/privacy": "4.1.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",