@form-engine-ts/react 4.3.2 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
30
30
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
31
31
  isTranslationUnresolved: () => isTranslationUnresolved,
32
+ resolveChoiceFieldLayout: () => resolveChoiceFieldLayout,
32
33
  resolveFieldEditorControls: () => resolveFieldEditorControls,
33
34
  resolveFieldTypeSelectOptions: () => resolveFieldTypeSelectOptions,
34
35
  resolveInitialFieldType: () => resolveInitialFieldType,
@@ -37,7 +38,8 @@ __export(index_exports, {
37
38
  useField: () => useField,
38
39
  useForm: () => useForm,
39
40
  useFormBuilder: () => useFormBuilder,
40
- useSubmissionReceipts: () => useSubmissionReceipts
41
+ useSubmissionReceipts: () => useSubmissionReceipts,
42
+ useTranslationWorkspace: () => useTranslationWorkspace
41
43
  });
42
44
  module.exports = __toCommonJS(index_exports);
43
45
 
@@ -233,8 +235,20 @@ function move(items, sourceIndex, targetIndex) {
233
235
  result.splice(targetIndex, 0, item);
234
236
  return result;
235
237
  }
238
+ function displayRuleSourceIds(field) {
239
+ if (field.displayRule === void 0) return [];
240
+ const ids = [];
241
+ const visit = (group) => {
242
+ for (const condition of group.conditions) {
243
+ if ("logic" in condition) visit(condition);
244
+ else ids.push(condition.fieldId);
245
+ }
246
+ };
247
+ visit(field.displayRule.condition);
248
+ return ids;
249
+ }
236
250
  function withoutDisplayCondition(field) {
237
- const { displayCondition: _displayCondition, ...rest } = field;
251
+ const { displayCondition: _displayCondition, displayRule: _displayRule, ...rest } = field;
238
252
  return rest;
239
253
  }
240
254
  function removeLocalizedProperty(translations, locale, property) {
@@ -257,8 +271,23 @@ function useFormBuilder({
257
271
  onChange,
258
272
  policy,
259
273
  idFactory = defaultIdFactory,
260
- factories = {}
274
+ factories = {},
275
+ fieldEditorMode = "all",
276
+ activeFieldId: controlledActiveFieldId,
277
+ defaultActiveFieldId,
278
+ onActiveFieldChange
261
279
  }) {
280
+ const [internalActiveFieldId, setInternalActiveFieldId] = (0, import_react.useState)(
281
+ defaultActiveFieldId ?? (fieldEditorMode === "single" ? schema.fields[0]?.id : void 0)
282
+ );
283
+ const activeFieldId = controlledActiveFieldId ?? internalActiveFieldId;
284
+ const setActiveFieldId = (0, import_react.useCallback)(
285
+ (fieldId) => {
286
+ if (controlledActiveFieldId === void 0) setInternalActiveFieldId(fieldId);
287
+ onActiveFieldChange?.(fieldId);
288
+ },
289
+ [controlledActiveFieldId, onActiveFieldChange]
290
+ );
262
291
  const createId = (0, import_react.useCallback)(
263
292
  (kind, existingIds) => {
264
293
  const rawId = idFactory(kind, existingIds);
@@ -371,9 +400,10 @@ function useFormBuilder({
371
400
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
372
401
  }));
373
402
  onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
403
+ setActiveFieldId(field.id);
374
404
  return { success: true };
375
405
  },
376
- [createId, factories, onChange, policy, schema]
406
+ [createId, factories, onChange, policy, schema, setActiveFieldId]
377
407
  );
378
408
  const removeField = (0, import_react.useCallback)(
379
409
  (fieldId) => {
@@ -381,15 +411,19 @@ function useFormBuilder({
381
411
  return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
382
412
  if (schema.fields.length <= 1)
383
413
  return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
384
- const fields = schema.fields.filter((field) => field.id !== fieldId).map((field) => field.displayCondition?.questionId === fieldId ? withoutDisplayCondition(field) : field);
414
+ const removedIndex = schema.fields.findIndex((field) => field.id === fieldId);
415
+ const fields = schema.fields.filter((field) => field.id !== fieldId).map(
416
+ (field) => field.displayCondition?.questionId === fieldId || displayRuleSourceIds(field).includes(fieldId) ? withoutDisplayCondition(field) : field
417
+ );
385
418
  const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
386
419
  if (schema.pages !== void 0 && pages?.length === 0) {
387
420
  const { pages: _pages, ...single } = schema;
388
421
  onChange({ ...single, fields });
389
422
  } else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
423
+ if (activeFieldId === fieldId) setActiveFieldId(fields[removedIndex]?.id ?? fields.at(-1)?.id);
390
424
  return { success: true };
391
425
  },
392
- [onChange, schema]
426
+ [activeFieldId, onChange, schema, setActiveFieldId]
393
427
  );
394
428
  const moveField = (0, import_react.useCallback)(
395
429
  (fieldId, targetIndex) => {
@@ -402,8 +436,8 @@ function useFormBuilder({
402
436
  onChange({
403
437
  ...schema,
404
438
  fields: fields.map((field, index) => {
405
- const source = field.displayCondition?.questionId;
406
- return source === void 0 || (indexById.get(source) ?? index) < index ? field : withoutDisplayCondition(field);
439
+ const sources = field.displayCondition?.questionId === void 0 ? displayRuleSourceIds(field) : [field.displayCondition.questionId];
440
+ return sources.every((source) => (indexById.get(source) ?? index) < index) ? field : withoutDisplayCondition(field);
407
441
  })
408
442
  });
409
443
  return { success: true };
@@ -827,6 +861,14 @@ function useFormBuilder({
827
861
  const result = (0, import_core.validateFormSchema)(schema, policy === void 0 ? {} : { policy });
828
862
  return result.valid ? [] : result.issues;
829
863
  }, [policy, schema]);
864
+ const getFieldEditorProps = (0, import_react.useCallback)(
865
+ (fieldId) => ({
866
+ isActive: activeFieldId === fieldId,
867
+ isVisible: fieldEditorMode === "all" || activeFieldId === fieldId,
868
+ onSelect: () => setActiveFieldId(fieldId)
869
+ }),
870
+ [activeFieldId, fieldEditorMode, setActiveFieldId]
871
+ );
830
872
  return {
831
873
  schema,
832
874
  addField,
@@ -848,7 +890,10 @@ function useFormBuilder({
848
890
  setLocaleTranslation,
849
891
  addLocale,
850
892
  setDefaultLocale,
851
- validationIssues
893
+ validationIssues,
894
+ ...activeFieldId === void 0 ? {} : { activeFieldId },
895
+ setActiveFieldId,
896
+ getFieldEditorProps
852
897
  };
853
898
  }
854
899
 
@@ -1501,7 +1546,10 @@ var BUILDER_DEFAULTS = {
1501
1546
  "builder.operator.equals": "equals",
1502
1547
  "builder.operator.not_equals": "does not equal",
1503
1548
  "builder.operator.contains": "contains",
1504
- "builder.operator.not_empty": "is not empty"
1549
+ "builder.operator.not_empty": "is not empty",
1550
+ "builder.submissionSettings": "Submission settings",
1551
+ "builder.showConfirmationBeforeSubmit": "Show confirmation before submit",
1552
+ "builder.confirmationRenderMode": "Confirmation display mode"
1505
1553
  };
1506
1554
  function BuilderSectionGroup({ children }) {
1507
1555
  return children;
@@ -1678,7 +1726,12 @@ function FormBuilder({
1678
1726
  slots,
1679
1727
  sectionOrder,
1680
1728
  disableDefaultStyles = false,
1681
- unstyled = false
1729
+ unstyled = false,
1730
+ fieldEditorMode = "all",
1731
+ activeFieldId,
1732
+ defaultActiveFieldId,
1733
+ onActiveFieldChange,
1734
+ submissionSettingsOptions
1682
1735
  }) {
1683
1736
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1684
1737
  const components = {
@@ -1701,7 +1754,11 @@ function FormBuilder({
1701
1754
  onChange,
1702
1755
  ...policy === void 0 ? {} : { policy },
1703
1756
  ...idFactory === void 0 ? {} : { idFactory },
1704
- ...factories === void 0 ? {} : { factories }
1757
+ ...factories === void 0 ? {} : { factories },
1758
+ fieldEditorMode,
1759
+ ...activeFieldId === void 0 ? {} : { activeFieldId },
1760
+ ...defaultActiveFieldId === void 0 ? {} : { defaultActiveFieldId },
1761
+ ...onActiveFieldChange === void 0 ? {} : { onActiveFieldChange }
1705
1762
  });
1706
1763
  const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
1707
1764
  const [newLocale, setNewLocale] = (0, import_react2.useState)("");
@@ -2041,6 +2098,47 @@ function FormBuilder({
2041
2098
  ] })
2042
2099
  }
2043
2100
  ) }),
2101
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "submissionSettings", children: submissionSettingsOptions?.enabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2102
+ Section,
2103
+ {
2104
+ className: builderClass("form-engine-builder__submission-settings"),
2105
+ title: translate("builder.submissionSettings"),
2106
+ children: [
2107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2108
+ Checkbox,
2109
+ {
2110
+ id: "builder-show-confirmation",
2111
+ label: translate("builder.showConfirmationBeforeSubmit"),
2112
+ checked: schema.submissionSettings?.showConfirmationBeforeSubmit === true,
2113
+ onChange: (checked) => onChange({
2114
+ ...schema,
2115
+ submissionSettings: { ...schema.submissionSettings, showConfirmationBeforeSubmit: checked }
2116
+ })
2117
+ }
2118
+ ),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2120
+ Select,
2121
+ {
2122
+ id: "builder-confirmation-render-mode",
2123
+ label: translate("builder.confirmationRenderMode"),
2124
+ value: schema.submissionSettings?.confirmationRenderMode ?? "inline",
2125
+ options: [
2126
+ { value: "dialog", label: "Dialog" },
2127
+ { value: "inline", label: "Inline" },
2128
+ { value: "replace", label: "Replace" }
2129
+ ],
2130
+ onChange: (value) => {
2131
+ if (value !== "dialog" && value !== "inline" && value !== "replace") return;
2132
+ onChange({
2133
+ ...schema,
2134
+ submissionSettings: { ...schema.submissionSettings, confirmationRenderMode: value }
2135
+ });
2136
+ }
2137
+ }
2138
+ )
2139
+ ]
2140
+ }
2141
+ ) : null }),
2044
2142
  resolvedSectionOrder === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "completionMessage", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { headingId: "builder-completion-message-heading", title: translate("builder.completionMessage"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2045
2143
  TextInput,
2046
2144
  {
@@ -2439,9 +2537,25 @@ function FormBuilder({
2439
2537
  ) : null }),
2440
2538
  /* @__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) => {
2441
2539
  const controls = resolveFieldEditorControls(fieldEditorControls);
2540
+ const editorState = headless.getFieldEditorProps?.(field.id) ?? {
2541
+ isActive: true,
2542
+ isVisible: true,
2543
+ onSelect: () => void 0
2544
+ };
2442
2545
  const condition = field.displayCondition;
2443
2546
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2444
2547
  const availableSources = schema.fields.slice(0, index);
2548
+ if (!editorState.isVisible) {
2549
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2550
+ "div",
2551
+ {
2552
+ className: builderClass("form-engine-builder__question-preview"),
2553
+ "data-field-id": field.id,
2554
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: editorState.onSelect, children: field.title })
2555
+ },
2556
+ field.id
2557
+ );
2558
+ }
2445
2559
  if (FieldEditorSlot !== void 0) {
2446
2560
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2447
2561
  FieldEditorSlot,
@@ -3149,8 +3263,232 @@ function useField(fieldId) {
3149
3263
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
3150
3264
  }
3151
3265
 
3152
- // src/receipt.ts
3266
+ // src/hooks/useTranslationWorkspace.ts
3267
+ var import_core4 = require("@form-engine-ts/core");
3153
3268
  var import_react4 = require("react");
3269
+ function updateTranslationMap(translations, locale, property, text) {
3270
+ if (property === "label") return translations ?? {};
3271
+ const current = translations?.[locale];
3272
+ const next = { ...current, [property]: text };
3273
+ return { ...translations, [locale]: next };
3274
+ }
3275
+ function asAsyncAdapter(adapter) {
3276
+ if ("translateBatch" in adapter) return adapter;
3277
+ return {
3278
+ translateText: async (text, locale, sourceLocale) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text,
3279
+ translateBatch: async (texts, locale, sourceLocale) => texts.map(
3280
+ (text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
3281
+ )
3282
+ };
3283
+ }
3284
+ function manualMetadata(sourceText, sourceLocale) {
3285
+ return {
3286
+ sourceLocale,
3287
+ sourceTextHash: (0, import_core4.computeSourceTextHash)(sourceText),
3288
+ translationSource: "manual",
3289
+ editedAt: (/* @__PURE__ */ new Date()).toISOString()
3290
+ };
3291
+ }
3292
+ function setNodeMetadata(node, slot, text, sourceLocale) {
3293
+ const localeMetadata = node.translationMetadata?.[slot.locale];
3294
+ const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
3295
+ return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
3296
+ }
3297
+ function updateSchemaTranslation(schema, slot, text, sourceLocale) {
3298
+ if (slot.kind === "form") {
3299
+ return setNodeMetadata(
3300
+ { ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
3301
+ slot,
3302
+ text,
3303
+ sourceLocale
3304
+ );
3305
+ }
3306
+ if (slot.kind === "field") {
3307
+ return {
3308
+ ...schema,
3309
+ fields: schema.fields.map((field) => {
3310
+ if (field.id !== slot.nodeId) return field;
3311
+ const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
3312
+ return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
3313
+ })
3314
+ };
3315
+ }
3316
+ if (slot.kind === "option") {
3317
+ return {
3318
+ ...schema,
3319
+ fields: schema.fields.map((field) => {
3320
+ if (!("options" in field)) return field;
3321
+ return {
3322
+ ...field,
3323
+ options: field.options.map((option) => {
3324
+ if (option.id !== slot.nodeId) return option;
3325
+ const translations = text.trim().length === 0 ? Object.fromEntries(
3326
+ Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
3327
+ ) : { ...option.translations, [slot.locale]: text };
3328
+ return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
3329
+ })
3330
+ };
3331
+ })
3332
+ };
3333
+ }
3334
+ return {
3335
+ ...schema,
3336
+ ...schema.pages === void 0 ? {} : {
3337
+ pages: schema.pages.map((page) => {
3338
+ if (page.id !== slot.nodeId) return page;
3339
+ const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
3340
+ return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
3341
+ })
3342
+ }
3343
+ };
3344
+ }
3345
+ function useTranslationWorkspace({
3346
+ schema,
3347
+ onChange,
3348
+ sourceLocale = schema.defaultLocale ?? "en",
3349
+ targetLocale,
3350
+ translationAdapter,
3351
+ readOnly = false
3352
+ }) {
3353
+ const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
3354
+ const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
3355
+ const [isTranslating, setIsTranslating] = (0, import_react4.useState)(false);
3356
+ const [error, setError] = (0, import_react4.useState)();
3357
+ const currentSchema = onChange === void 0 ? draftSchema : schema;
3358
+ const targetLocales = (0, import_react4.useMemo)(() => {
3359
+ const locales = (0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales;
3360
+ return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
3361
+ (locale) => locale !== sourceLocale
3362
+ );
3363
+ }, [currentSchema, sourceLocale]);
3364
+ const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
3365
+ const slots = (0, import_react4.useMemo)(
3366
+ () => activeLocale.length === 0 ? [] : (0, import_core4.collectTranslationSlots)(currentSchema, activeLocale),
3367
+ [activeLocale, currentSchema]
3368
+ );
3369
+ const summary = (0, import_react4.useMemo)(() => {
3370
+ const counts = {
3371
+ missing: 0,
3372
+ translated: 0,
3373
+ stale: 0,
3374
+ manual: 0,
3375
+ "manual-stale": 0
3376
+ };
3377
+ for (const slot of slots) counts[slot.status ?? "missing"] += 1;
3378
+ const complete = counts.translated + counts.manual;
3379
+ return {
3380
+ totalSlots: slots.length,
3381
+ translatedCount: complete,
3382
+ missingCount: counts.missing,
3383
+ staleCount: counts.stale + counts["manual-stale"],
3384
+ manualCount: counts.manual + counts["manual-stale"],
3385
+ completionPercentage: slots.length === 0 ? 100 : Math.round(complete / slots.length * 100)
3386
+ };
3387
+ }, [slots]);
3388
+ const commit = (0, import_react4.useCallback)(
3389
+ (next) => {
3390
+ setDraftSchema(next);
3391
+ onChange?.(next);
3392
+ },
3393
+ [onChange]
3394
+ );
3395
+ const setTranslation = (0, import_react4.useCallback)(
3396
+ (slot, text) => {
3397
+ if (readOnly) return;
3398
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3399
+ },
3400
+ [commit, currentSchema, readOnly, sourceLocale]
3401
+ );
3402
+ const addLocale = (0, import_react4.useCallback)(
3403
+ (locale) => {
3404
+ if (readOnly || locale.trim().length === 0) return;
3405
+ const normalized = locale.trim();
3406
+ commit({
3407
+ ...currentSchema,
3408
+ supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
3409
+ });
3410
+ setSelectedLocale(normalized);
3411
+ },
3412
+ [commit, currentSchema, readOnly]
3413
+ );
3414
+ const removeLocale = (0, import_react4.useCallback)(
3415
+ (locale) => {
3416
+ if (readOnly || locale === sourceLocale) return;
3417
+ commit({
3418
+ ...currentSchema,
3419
+ supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
3420
+ });
3421
+ if (activeLocale === locale) setSelectedLocale("");
3422
+ },
3423
+ [activeLocale, commit, currentSchema, readOnly, sourceLocale]
3424
+ );
3425
+ const translateAll = (0, import_react4.useCallback)(
3426
+ async (options = {}) => {
3427
+ if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
3428
+ if (activeLocale.length === 0) throw new Error("A target locale is required.");
3429
+ setIsTranslating(true);
3430
+ setError(void 0);
3431
+ try {
3432
+ const populated = await (0, import_core4.populateSchemaTranslations)(
3433
+ currentSchema,
3434
+ [activeLocale],
3435
+ asAsyncAdapter(translationAdapter),
3436
+ {
3437
+ overwrite: "stale-and-missing",
3438
+ preserveManualTranslations: true,
3439
+ ...options
3440
+ }
3441
+ );
3442
+ commit(populated.schema);
3443
+ return populated.report;
3444
+ } catch (cause) {
3445
+ const message = cause instanceof Error ? cause.message : String(cause);
3446
+ setError(message);
3447
+ throw cause;
3448
+ } finally {
3449
+ setIsTranslating(false);
3450
+ }
3451
+ },
3452
+ [activeLocale, commit, currentSchema, translationAdapter]
3453
+ );
3454
+ const translateSlot = (0, import_react4.useCallback)(
3455
+ async (slot) => {
3456
+ if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
3457
+ if (readOnly) return;
3458
+ setIsTranslating(true);
3459
+ setError(void 0);
3460
+ try {
3461
+ const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
3462
+ commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
3463
+ } catch (cause) {
3464
+ const message = cause instanceof Error ? cause.message : String(cause);
3465
+ setError(message);
3466
+ throw cause;
3467
+ } finally {
3468
+ setIsTranslating(false);
3469
+ }
3470
+ },
3471
+ [commit, currentSchema, readOnly, sourceLocale, translationAdapter]
3472
+ );
3473
+ return {
3474
+ sourceLocale,
3475
+ targetLocale: activeLocale,
3476
+ targetLocales,
3477
+ setTargetLocale: setSelectedLocale,
3478
+ slots,
3479
+ summary,
3480
+ addLocale,
3481
+ removeLocale,
3482
+ setTranslation,
3483
+ translateAll,
3484
+ translateSlot,
3485
+ isTranslating,
3486
+ ...error === void 0 ? {} : { error }
3487
+ };
3488
+ }
3489
+
3490
+ // src/receipt.ts
3491
+ var import_react5 = require("react");
3154
3492
  function submissionReceiptQueryKey(formId, formVersion) {
3155
3493
  return `${formId}:v${formVersion}`;
3156
3494
  }
@@ -3221,19 +3559,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3221
3559
  }
3222
3560
  function useSubmissionReceipts(store, queries) {
3223
3561
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3224
- const stableQueries = (0, import_react4.useMemo)(() => {
3562
+ const stableQueries = (0, import_react5.useMemo)(() => {
3225
3563
  const parsed = JSON.parse(querySignature);
3226
3564
  if (!Array.isArray(parsed)) return [];
3227
3565
  return parsed.flatMap(
3228
3566
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3229
3567
  );
3230
3568
  }, [querySignature]);
3231
- const [state, setState] = (0, import_react4.useState)({
3569
+ const [state, setState] = (0, import_react5.useState)({
3232
3570
  receipts: /* @__PURE__ */ new Map(),
3233
3571
  isLoading: stableQueries.length > 0,
3234
3572
  error: null
3235
3573
  });
3236
- (0, import_react4.useEffect)(() => {
3574
+ (0, import_react5.useEffect)(() => {
3237
3575
  let active = true;
3238
3576
  if (stableQueries.length === 0) {
3239
3577
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3267,9 +3605,17 @@ function useSubmissionReceipts(store, queries) {
3267
3605
  }
3268
3606
 
3269
3607
  // src/renderer.tsx
3270
- var import_core4 = require("@form-engine-ts/core");
3271
- var import_react5 = require("react");
3608
+ var import_core5 = require("@form-engine-ts/core");
3609
+ var import_react6 = require("react");
3272
3610
  var import_jsx_runtime3 = require("react/jsx-runtime");
3611
+ var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3612
+ function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
3613
+ if (groupedChoiceFieldsLegacy) return "grouped";
3614
+ const config = appearance?.choiceField;
3615
+ if (config === void 0) return "default";
3616
+ if (typeof config === "string") return config;
3617
+ return config[type] ?? "default";
3618
+ }
3273
3619
  function describedBy(field, error, helpId, errorId) {
3274
3620
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
3275
3621
  Boolean
@@ -3297,51 +3643,98 @@ function GroupedChoiceDescription({ props }) {
3297
3643
  function GroupedChoiceError({ props }) {
3298
3644
  return props.error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.errorId, className: "fe-field-error", role: "alert", children: props.translate(props.error.messageKey, props.error.params) });
3299
3645
  }
3646
+ function ChoiceGroupFrame({
3647
+ props,
3648
+ children,
3649
+ className,
3650
+ slotProps,
3651
+ renderChoiceGroup,
3652
+ disabled,
3653
+ readOnly
3654
+ }) {
3655
+ const { field, error, translate } = props;
3656
+ const groupError = error === void 0 ? void 0 : { ...error, message: translate(error.messageKey, error.params) };
3657
+ const groupProps = {
3658
+ field,
3659
+ title: field.title,
3660
+ ...field.description === void 0 ? {} : { description: field.description },
3661
+ ...field.required === void 0 ? {} : { required: field.required },
3662
+ ...groupError === void 0 ? {} : { error: groupError },
3663
+ ...disabled === void 0 ? {} : { disabled },
3664
+ ...readOnly === void 0 ? {} : { readOnly },
3665
+ children,
3666
+ className
3667
+ };
3668
+ if (renderChoiceGroup !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderChoiceGroup(groupProps) });
3669
+ return (
3670
+ // biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
3671
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3672
+ "fieldset",
3673
+ {
3674
+ className,
3675
+ "data-field-id": field.id,
3676
+ disabled,
3677
+ "aria-describedby": describedBy(field, error, props.helpId, props.errorId),
3678
+ "aria-invalid": Boolean(error),
3679
+ "aria-required": field.required,
3680
+ style: slotProps?.style,
3681
+ children: [
3682
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
3683
+ field.title,
3684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
3685
+ ] }),
3686
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
3687
+ children,
3688
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
3689
+ ]
3690
+ }
3691
+ )
3692
+ );
3693
+ }
3300
3694
  function DefaultField({
3301
3695
  groupedChoiceFields,
3696
+ appearance,
3697
+ choiceGroupSlotProps,
3698
+ renderChoiceGroup,
3699
+ disabled,
3700
+ readOnly,
3302
3701
  ...props
3303
3702
  }) {
3304
3703
  const { field, value, setValue, inputId, error, translate } = props;
3704
+ const isGroupedChoiceField = isChoiceFieldType(field.type) && resolveChoiceFieldLayout(field.type, appearance, groupedChoiceFields) === "grouped";
3705
+ const choiceGroupClassName = ["fe-choice-group", `fe-field--${field.type}`, choiceGroupSlotProps?.className].filter((item) => item !== void 0 && item.length > 0).join(" ");
3305
3706
  const ariaProps = {
3306
3707
  "aria-describedby": describedBy(field, error, props.helpId, props.errorId),
3307
3708
  "aria-invalid": error === void 0 ? void 0 : true
3308
3709
  };
3309
3710
  if (field.type === "checkbox") {
3310
- if (groupedChoiceFields) {
3311
- return (
3312
- // biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
3313
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3314
- "fieldset",
3315
- {
3316
- className: "fe-choice-group fe-field--checkbox",
3317
- "data-field-id": field.id,
3318
- "aria-describedby": describedBy(field, error, props.helpId, props.errorId),
3319
- "aria-invalid": Boolean(error),
3320
- "aria-required": field.required,
3321
- children: [
3322
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
3323
- field.title,
3324
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
3325
- ] }),
3326
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
3327
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: inputId, children: [
3328
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3329
- "input",
3330
- {
3331
- id: inputId,
3332
- name: field.id,
3333
- type: "checkbox",
3334
- checked: value === true,
3335
- "aria-label": field.title,
3336
- onChange: (event) => setValue(event.currentTarget.checked)
3337
- }
3338
- ),
3339
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
3340
- ] }) }),
3341
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
3342
- ]
3343
- }
3344
- )
3711
+ if (isGroupedChoiceField) {
3712
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3713
+ ChoiceGroupFrame,
3714
+ {
3715
+ props,
3716
+ className: choiceGroupClassName,
3717
+ slotProps: choiceGroupSlotProps,
3718
+ renderChoiceGroup,
3719
+ disabled,
3720
+ readOnly,
3721
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: inputId, children: [
3722
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3723
+ "input",
3724
+ {
3725
+ id: inputId,
3726
+ name: field.id,
3727
+ type: "checkbox",
3728
+ checked: value === true,
3729
+ "aria-label": field.title,
3730
+ disabled,
3731
+ readOnly,
3732
+ onChange: (event) => setValue(event.currentTarget.checked)
3733
+ }
3734
+ ),
3735
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
3736
+ ] }) })
3737
+ }
3345
3738
  );
3346
3739
  }
3347
3740
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
@@ -3367,56 +3760,55 @@ function DefaultField({
3367
3760
  }
3368
3761
  if (field.type === "radio" || field.type === "multi-select") {
3369
3762
  const selected = Array.isArray(value) ? value : [];
3370
- if (field.type === "radio" && groupedChoiceFields) {
3371
- return (
3372
- // biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
3373
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3374
- "fieldset",
3375
- {
3376
- className: "fe-choice-group fe-field--radio",
3377
- "data-field-id": field.id,
3378
- "aria-describedby": describedBy(field, error, props.helpId, props.errorId),
3379
- "aria-invalid": Boolean(error),
3380
- "aria-required": field.required,
3381
- children: [
3382
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
3383
- field.title,
3384
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
3385
- ] }),
3386
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
3387
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
3388
- const optionId = `${inputId}-${index}`;
3389
- const checked = value === option.id;
3390
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: optionId, children: [
3391
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3392
- "input",
3393
- {
3394
- id: optionId,
3395
- name: field.id,
3396
- type: "radio",
3397
- value: option.id,
3398
- checked,
3399
- onKeyDown: (event) => {
3400
- if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
3401
- return;
3402
- event.preventDefault();
3403
- const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
3404
- const nextIndex = (index + offset + field.options.length) % field.options.length;
3405
- const nextOption = field.options[nextIndex];
3406
- if (nextOption === void 0) return;
3407
- setValue(nextOption.id);
3408
- document.getElementById(`${inputId}-${nextIndex}`)?.focus();
3409
- },
3410
- onChange: () => setValue(option.id)
3411
- }
3412
- ),
3413
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
3414
- ] }, option.id);
3415
- }) }),
3416
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
3417
- ]
3418
- }
3419
- )
3763
+ if (isGroupedChoiceField) {
3764
+ const isRadio = field.type === "radio";
3765
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3766
+ ChoiceGroupFrame,
3767
+ {
3768
+ props,
3769
+ className: choiceGroupClassName,
3770
+ slotProps: choiceGroupSlotProps,
3771
+ renderChoiceGroup,
3772
+ disabled,
3773
+ readOnly,
3774
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
3775
+ const optionId = `${inputId}-${index}`;
3776
+ const checked = isRadio ? value === option.id : selected.includes(option.id);
3777
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: optionId, children: [
3778
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3779
+ "input",
3780
+ {
3781
+ id: optionId,
3782
+ name: field.id,
3783
+ type: isRadio ? "radio" : "checkbox",
3784
+ value: option.id,
3785
+ checked,
3786
+ disabled,
3787
+ readOnly,
3788
+ onKeyDown: isRadio ? (event) => {
3789
+ if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
3790
+ return;
3791
+ event.preventDefault();
3792
+ const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
3793
+ const nextIndex = (index + offset + field.options.length) % field.options.length;
3794
+ const nextOption = field.options[nextIndex];
3795
+ if (nextOption === void 0) return;
3796
+ setValue(nextOption.id);
3797
+ document.getElementById(`${inputId}-${nextIndex}`)?.focus();
3798
+ } : void 0,
3799
+ onChange: (event) => {
3800
+ if (isRadio) setValue(option.id);
3801
+ else
3802
+ setValue(
3803
+ event.currentTarget.checked ? [...selected, option.id] : selected.filter((item) => item !== option.id)
3804
+ );
3805
+ }
3806
+ }
3807
+ ),
3808
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
3809
+ ] }, option.id);
3810
+ }) })
3811
+ }
3420
3812
  );
3421
3813
  }
3422
3814
  if (field.type === "radio") {
@@ -3553,6 +3945,7 @@ function DefaultField({
3553
3945
  ...ariaProps,
3554
3946
  id: inputId,
3555
3947
  name: field.id,
3948
+ disabled,
3556
3949
  value: typeof value === "string" ? value : "",
3557
3950
  onChange: (event) => setValue(event.currentTarget.value || void 0),
3558
3951
  children: [
@@ -3576,6 +3969,20 @@ function DefaultField({
3576
3969
  }
3577
3970
  );
3578
3971
  }
3972
+ if (isGroupedChoiceField) {
3973
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3974
+ ChoiceGroupFrame,
3975
+ {
3976
+ props,
3977
+ className: choiceGroupClassName,
3978
+ slotProps: choiceGroupSlotProps,
3979
+ renderChoiceGroup,
3980
+ disabled,
3981
+ readOnly,
3982
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: control })
3983
+ }
3984
+ );
3985
+ }
3579
3986
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
3580
3987
  label,
3581
3988
  control,
@@ -3696,6 +4103,7 @@ function ContextFormRenderer({
3696
4103
  onDraftSave,
3697
4104
  successRenderMode = "append",
3698
4105
  appearance,
4106
+ slotProps,
3699
4107
  groupedChoiceFields = false,
3700
4108
  submissionConfirmation,
3701
4109
  submissionConfirmationRenderMode,
@@ -3709,42 +4117,41 @@ function ContextFormRenderer({
3709
4117
  slots = {}
3710
4118
  }) {
3711
4119
  const form = useForm();
3712
- const isGroupedMode = appearance?.choiceField === "grouped" || groupedChoiceFields;
3713
- const prefix = (0, import_react5.useId)().replace(/:/g, "");
3714
- const formRef = (0, import_react5.useRef)(null);
3715
- const loadedDraftKey = (0, import_react5.useRef)(null);
3716
- const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
3717
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
3718
- const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
3719
- const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
3720
- const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
3721
- const [guardsPending, setGuardsPending] = (0, import_react5.useState)(false);
3722
- const [receipt, setReceipt] = (0, import_react5.useState)(null);
3723
- const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3724
- const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3725
- const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3726
- const fallbackAttemptId = (0, import_react5.useRef)(null);
3727
- const completionRef = (0, import_react5.useRef)(null);
3728
- const confirmationRef = (0, import_react5.useRef)(null);
4120
+ const prefix = (0, import_react6.useId)().replace(/:/g, "");
4121
+ const formRef = (0, import_react6.useRef)(null);
4122
+ const loadedDraftKey = (0, import_react6.useRef)(null);
4123
+ const [draftRestored, setDraftRestored] = (0, import_react6.useState)(false);
4124
+ const [currentPageIndex, setCurrentPageIndex] = (0, import_react6.useState)(0);
4125
+ const [focusFieldId, setFocusFieldId] = (0, import_react6.useState)(null);
4126
+ const [confirmation, setConfirmation] = (0, import_react6.useState)(null);
4127
+ const [guardMessage, setGuardMessage] = (0, import_react6.useState)(null);
4128
+ const [guardsPending, setGuardsPending] = (0, import_react6.useState)(false);
4129
+ const [receipt, setReceipt] = (0, import_react6.useState)(null);
4130
+ const [completionData, setCompletionData] = (0, import_react6.useState)(null);
4131
+ const [receiptLoaded, setReceiptLoaded] = (0, import_react6.useState)(receiptStore === void 0);
4132
+ const rendererSubmissionInFlight = (0, import_react6.useRef)(false);
4133
+ const fallbackAttemptId = (0, import_react6.useRef)(null);
4134
+ const completionRef = (0, import_react6.useRef)(null);
4135
+ const confirmationRef = (0, import_react6.useRef)(null);
3729
4136
  const pages = form.schema.pages;
3730
- const visiblePageIndexes = (0, import_react5.useMemo)(
4137
+ const visiblePageIndexes = (0, import_react6.useMemo)(
3731
4138
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
3732
4139
  [form.pageVisibility, pages]
3733
4140
  );
3734
4141
  const activePage = pages?.[currentPageIndex];
3735
4142
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
3736
4143
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
3737
- const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
3738
- const visibleItems = (0, import_react5.useMemo)(
4144
+ const visibleValues = (0, import_react6.useMemo)(() => (0, import_core5.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
4145
+ const visibleItems = (0, import_react6.useMemo)(
3739
4146
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
3740
4147
  [form.schema, form.translate, form.values, form.visibility]
3741
4148
  );
3742
- const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
3743
- const confirmationEnabled = submissionConfirmation?.enabled === true;
4149
+ const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? form.schema.submissionSettings?.confirmationRenderMode ?? "inline";
4150
+ const confirmationEnabled = submissionConfirmation?.enabled ?? form.schema.submissionSettings?.showConfirmationBeforeSubmit ?? false;
3744
4151
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3745
4152
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3746
4153
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3747
- const resolveMessage = (0, import_react5.useCallback)(
4154
+ const resolveMessage = (0, import_react6.useCallback)(
3748
4155
  (key, fallback) => {
3749
4156
  const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3750
4157
  const configured = messages[key];
@@ -3752,15 +4159,15 @@ function ContextFormRenderer({
3752
4159
  },
3753
4160
  [form.locale, messageResolver, messages]
3754
4161
  );
3755
- const fieldTranslate = (0, import_react5.useCallback)(
4162
+ const fieldTranslate = (0, import_react6.useCallback)(
3756
4163
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3757
4164
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
3758
4165
  );
3759
- const focusSubmitButton = (0, import_react5.useCallback)(() => {
4166
+ const focusSubmitButton = (0, import_react6.useCallback)(() => {
3760
4167
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3761
4168
  button?.focus();
3762
4169
  }, []);
3763
- (0, import_react5.useEffect)(() => {
4170
+ (0, import_react6.useEffect)(() => {
3764
4171
  let active = true;
3765
4172
  if (receiptStore === void 0) {
3766
4173
  setReceipt(null);
@@ -3781,14 +4188,14 @@ function ContextFormRenderer({
3781
4188
  active = false;
3782
4189
  };
3783
4190
  }, [form.schema.id, form.schema.version, receiptStore]);
3784
- (0, import_react5.useEffect)(() => {
4191
+ (0, import_react6.useEffect)(() => {
3785
4192
  if (pages === void 0 || visiblePageIndexes.length === 0) {
3786
4193
  setCurrentPageIndex(0);
3787
4194
  return;
3788
4195
  }
3789
4196
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
3790
4197
  }, [currentPageIndex, pages, visiblePageIndexes]);
3791
- (0, import_react5.useEffect)(() => {
4198
+ (0, import_react6.useEffect)(() => {
3792
4199
  if (focusFieldId === null) return;
3793
4200
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
3794
4201
  (element) => element.dataset.fieldId === focusFieldId
@@ -3800,11 +4207,11 @@ function ContextFormRenderer({
3800
4207
  setFocusFieldId(null);
3801
4208
  }
3802
4209
  }, [focusFieldId]);
3803
- (0, import_react5.useEffect)(() => {
4210
+ (0, import_react6.useEffect)(() => {
3804
4211
  if (!isReplaceMode || form.submitStatus !== "success") return;
3805
4212
  completionRef.current?.focus();
3806
4213
  }, [form.submitStatus, isReplaceMode]);
3807
- (0, import_react5.useEffect)(() => {
4214
+ (0, import_react6.useEffect)(() => {
3808
4215
  if (confirmation === null) return;
3809
4216
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
3810
4217
  confirmButton?.focus();
@@ -3836,7 +4243,7 @@ function ContextFormRenderer({
3836
4243
  globalThis.addEventListener("keydown", onKeyDown);
3837
4244
  return () => globalThis.removeEventListener("keydown", onKeyDown);
3838
4245
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
3839
- (0, import_react5.useEffect)(() => {
4246
+ (0, import_react6.useEffect)(() => {
3840
4247
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3841
4248
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
3842
4249
  if (loadedDraftKey.current === loadIdentity) return;
@@ -3848,7 +4255,7 @@ function ContextFormRenderer({
3848
4255
  form.restoreValues(draft.values);
3849
4256
  setDraftRestored(true);
3850
4257
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
3851
- (0, import_react5.useEffect)(() => {
4258
+ (0, import_react6.useEffect)(() => {
3852
4259
  if (form.submitStatus === "success") return;
3853
4260
  const timeout = globalThis.setTimeout(() => {
3854
4261
  onDraftSave?.(form.values);
@@ -3904,7 +4311,7 @@ function ContextFormRenderer({
3904
4311
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
3905
4312
  return { status: "cancelled" };
3906
4313
  }
3907
- const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
4314
+ const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
3908
4315
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
3909
4316
  if (validation.valid && !guardsConfirmed) {
3910
4317
  rendererSubmissionInFlight.current = true;
@@ -4206,7 +4613,7 @@ function ContextFormRenderer({
4206
4613
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4207
4614
  };
4208
4615
  if (slots.renderField !== void 0) {
4209
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react5.Fragment, { children: slots.renderField({
4616
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react6.Fragment, { children: slots.renderField({
4210
4617
  question: field,
4211
4618
  value: form.values[field.id],
4212
4619
  onChange: (value) => {
@@ -4216,7 +4623,18 @@ function ContextFormRenderer({
4216
4623
  }) }, field.id);
4217
4624
  }
4218
4625
  const Component = components[field.type];
4219
- return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props, groupedChoiceFields: isGroupedMode }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
4626
+ return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4627
+ DefaultField,
4628
+ {
4629
+ ...props,
4630
+ groupedChoiceFields,
4631
+ appearance,
4632
+ choiceGroupSlotProps: slotProps?.choiceGroup,
4633
+ renderChoiceGroup: slots.renderChoiceGroup,
4634
+ disabled: interactionLocked
4635
+ },
4636
+ field.id
4637
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
4220
4638
  });
4221
4639
  const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
4222
4640
  return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
@@ -4356,6 +4774,7 @@ function FormRenderer(props) {
4356
4774
  createLocalStorageSubmissionAttemptStore,
4357
4775
  createLocalStorageSubmissionReceiptStore,
4358
4776
  isTranslationUnresolved,
4777
+ resolveChoiceFieldLayout,
4359
4778
  resolveFieldEditorControls,
4360
4779
  resolveFieldTypeSelectOptions,
4361
4780
  resolveInitialFieldType,
@@ -4364,5 +4783,6 @@ function FormRenderer(props) {
4364
4783
  useField,
4365
4784
  useForm,
4366
4785
  useFormBuilder,
4367
- useSubmissionReceipts
4786
+ useSubmissionReceipts,
4787
+ useTranslationWorkspace
4368
4788
  });