@form-engine-ts/react 4.4.0 → 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/README.md CHANGED
@@ -73,6 +73,12 @@ on the next mount:
73
73
  membership controls and localization editors; pass an `AsyncTranslationAdapter` as `translationAdapter` to enable its
74
74
  batch-translation action.
75
75
 
76
+ `useTranslationWorkspace` exposes locale management, translation slots, completion summaries, manual editing, and
77
+ single-slot or batch translation with stale/manual status handling. The MUI package exports a ready-made
78
+ `TranslationWorkspace` surface. For large forms, set `fieldEditorMode="single"` and use `activeFieldId` or the hook's
79
+ `setActiveFieldId` to keep one field editor open at a time. Set `submissionSettingsOptions={{ enabled: true }}` to expose
80
+ schema-driven pre-submit confirmation controls in the builder.
81
+
76
82
  ## Headless builder and renderer lifecycle
77
83
 
78
84
  `useFormBuilder({ schema, onChange, policy, idFactory, factories })` exposes controlled field, option, page, condition,
package/dist/index.cjs CHANGED
@@ -38,7 +38,8 @@ __export(index_exports, {
38
38
  useField: () => useField,
39
39
  useForm: () => useForm,
40
40
  useFormBuilder: () => useFormBuilder,
41
- useSubmissionReceipts: () => useSubmissionReceipts
41
+ useSubmissionReceipts: () => useSubmissionReceipts,
42
+ useTranslationWorkspace: () => useTranslationWorkspace
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
45
 
@@ -234,8 +235,20 @@ function move(items, sourceIndex, targetIndex) {
234
235
  result.splice(targetIndex, 0, item);
235
236
  return result;
236
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
+ }
237
250
  function withoutDisplayCondition(field) {
238
- const { displayCondition: _displayCondition, ...rest } = field;
251
+ const { displayCondition: _displayCondition, displayRule: _displayRule, ...rest } = field;
239
252
  return rest;
240
253
  }
241
254
  function removeLocalizedProperty(translations, locale, property) {
@@ -258,8 +271,23 @@ function useFormBuilder({
258
271
  onChange,
259
272
  policy,
260
273
  idFactory = defaultIdFactory,
261
- factories = {}
274
+ factories = {},
275
+ fieldEditorMode = "all",
276
+ activeFieldId: controlledActiveFieldId,
277
+ defaultActiveFieldId,
278
+ onActiveFieldChange
262
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
+ );
263
291
  const createId = (0, import_react.useCallback)(
264
292
  (kind, existingIds) => {
265
293
  const rawId = idFactory(kind, existingIds);
@@ -372,9 +400,10 @@ function useFormBuilder({
372
400
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
373
401
  }));
374
402
  onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
403
+ setActiveFieldId(field.id);
375
404
  return { success: true };
376
405
  },
377
- [createId, factories, onChange, policy, schema]
406
+ [createId, factories, onChange, policy, schema, setActiveFieldId]
378
407
  );
379
408
  const removeField = (0, import_react.useCallback)(
380
409
  (fieldId) => {
@@ -382,15 +411,19 @@ function useFormBuilder({
382
411
  return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
383
412
  if (schema.fields.length <= 1)
384
413
  return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
385
- 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
+ );
386
418
  const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
387
419
  if (schema.pages !== void 0 && pages?.length === 0) {
388
420
  const { pages: _pages, ...single } = schema;
389
421
  onChange({ ...single, fields });
390
422
  } else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
423
+ if (activeFieldId === fieldId) setActiveFieldId(fields[removedIndex]?.id ?? fields.at(-1)?.id);
391
424
  return { success: true };
392
425
  },
393
- [onChange, schema]
426
+ [activeFieldId, onChange, schema, setActiveFieldId]
394
427
  );
395
428
  const moveField = (0, import_react.useCallback)(
396
429
  (fieldId, targetIndex) => {
@@ -403,8 +436,8 @@ function useFormBuilder({
403
436
  onChange({
404
437
  ...schema,
405
438
  fields: fields.map((field, index) => {
406
- const source = field.displayCondition?.questionId;
407
- 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);
408
441
  })
409
442
  });
410
443
  return { success: true };
@@ -828,6 +861,14 @@ function useFormBuilder({
828
861
  const result = (0, import_core.validateFormSchema)(schema, policy === void 0 ? {} : { policy });
829
862
  return result.valid ? [] : result.issues;
830
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
+ );
831
872
  return {
832
873
  schema,
833
874
  addField,
@@ -849,7 +890,10 @@ function useFormBuilder({
849
890
  setLocaleTranslation,
850
891
  addLocale,
851
892
  setDefaultLocale,
852
- validationIssues
893
+ validationIssues,
894
+ ...activeFieldId === void 0 ? {} : { activeFieldId },
895
+ setActiveFieldId,
896
+ getFieldEditorProps
853
897
  };
854
898
  }
855
899
 
@@ -1502,7 +1546,10 @@ var BUILDER_DEFAULTS = {
1502
1546
  "builder.operator.equals": "equals",
1503
1547
  "builder.operator.not_equals": "does not equal",
1504
1548
  "builder.operator.contains": "contains",
1505
- "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"
1506
1553
  };
1507
1554
  function BuilderSectionGroup({ children }) {
1508
1555
  return children;
@@ -1679,7 +1726,12 @@ function FormBuilder({
1679
1726
  slots,
1680
1727
  sectionOrder,
1681
1728
  disableDefaultStyles = false,
1682
- unstyled = false
1729
+ unstyled = false,
1730
+ fieldEditorMode = "all",
1731
+ activeFieldId,
1732
+ defaultActiveFieldId,
1733
+ onActiveFieldChange,
1734
+ submissionSettingsOptions
1683
1735
  }) {
1684
1736
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1685
1737
  const components = {
@@ -1702,7 +1754,11 @@ function FormBuilder({
1702
1754
  onChange,
1703
1755
  ...policy === void 0 ? {} : { policy },
1704
1756
  ...idFactory === void 0 ? {} : { idFactory },
1705
- ...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 }
1706
1762
  });
1707
1763
  const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
1708
1764
  const [newLocale, setNewLocale] = (0, import_react2.useState)("");
@@ -2042,6 +2098,47 @@ function FormBuilder({
2042
2098
  ] })
2043
2099
  }
2044
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 }),
2045
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)(
2046
2143
  TextInput,
2047
2144
  {
@@ -2440,9 +2537,25 @@ function FormBuilder({
2440
2537
  ) : null }),
2441
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) => {
2442
2539
  const controls = resolveFieldEditorControls(fieldEditorControls);
2540
+ const editorState = headless.getFieldEditorProps?.(field.id) ?? {
2541
+ isActive: true,
2542
+ isVisible: true,
2543
+ onSelect: () => void 0
2544
+ };
2443
2545
  const condition = field.displayCondition;
2444
2546
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2445
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
+ }
2446
2559
  if (FieldEditorSlot !== void 0) {
2447
2560
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2448
2561
  FieldEditorSlot,
@@ -3150,8 +3263,232 @@ function useField(fieldId) {
3150
3263
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
3151
3264
  }
3152
3265
 
3153
- // src/receipt.ts
3266
+ // src/hooks/useTranslationWorkspace.ts
3267
+ var import_core4 = require("@form-engine-ts/core");
3154
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");
3155
3492
  function submissionReceiptQueryKey(formId, formVersion) {
3156
3493
  return `${formId}:v${formVersion}`;
3157
3494
  }
@@ -3222,19 +3559,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
3222
3559
  }
3223
3560
  function useSubmissionReceipts(store, queries) {
3224
3561
  const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
3225
- const stableQueries = (0, import_react4.useMemo)(() => {
3562
+ const stableQueries = (0, import_react5.useMemo)(() => {
3226
3563
  const parsed = JSON.parse(querySignature);
3227
3564
  if (!Array.isArray(parsed)) return [];
3228
3565
  return parsed.flatMap(
3229
3566
  (entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
3230
3567
  );
3231
3568
  }, [querySignature]);
3232
- const [state, setState] = (0, import_react4.useState)({
3569
+ const [state, setState] = (0, import_react5.useState)({
3233
3570
  receipts: /* @__PURE__ */ new Map(),
3234
3571
  isLoading: stableQueries.length > 0,
3235
3572
  error: null
3236
3573
  });
3237
- (0, import_react4.useEffect)(() => {
3574
+ (0, import_react5.useEffect)(() => {
3238
3575
  let active = true;
3239
3576
  if (stableQueries.length === 0) {
3240
3577
  setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
@@ -3268,8 +3605,8 @@ function useSubmissionReceipts(store, queries) {
3268
3605
  }
3269
3606
 
3270
3607
  // src/renderer.tsx
3271
- var import_core4 = require("@form-engine-ts/core");
3272
- var import_react5 = require("react");
3608
+ var import_core5 = require("@form-engine-ts/core");
3609
+ var import_react6 = require("react");
3273
3610
  var import_jsx_runtime3 = require("react/jsx-runtime");
3274
3611
  var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
3275
3612
  function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
@@ -3780,41 +4117,41 @@ function ContextFormRenderer({
3780
4117
  slots = {}
3781
4118
  }) {
3782
4119
  const form = useForm();
3783
- const prefix = (0, import_react5.useId)().replace(/:/g, "");
3784
- const formRef = (0, import_react5.useRef)(null);
3785
- const loadedDraftKey = (0, import_react5.useRef)(null);
3786
- const [draftRestored, setDraftRestored] = (0, import_react5.useState)(false);
3787
- const [currentPageIndex, setCurrentPageIndex] = (0, import_react5.useState)(0);
3788
- const [focusFieldId, setFocusFieldId] = (0, import_react5.useState)(null);
3789
- const [confirmation, setConfirmation] = (0, import_react5.useState)(null);
3790
- const [guardMessage, setGuardMessage] = (0, import_react5.useState)(null);
3791
- const [guardsPending, setGuardsPending] = (0, import_react5.useState)(false);
3792
- const [receipt, setReceipt] = (0, import_react5.useState)(null);
3793
- const [completionData, setCompletionData] = (0, import_react5.useState)(null);
3794
- const [receiptLoaded, setReceiptLoaded] = (0, import_react5.useState)(receiptStore === void 0);
3795
- const rendererSubmissionInFlight = (0, import_react5.useRef)(false);
3796
- const fallbackAttemptId = (0, import_react5.useRef)(null);
3797
- const completionRef = (0, import_react5.useRef)(null);
3798
- 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);
3799
4136
  const pages = form.schema.pages;
3800
- const visiblePageIndexes = (0, import_react5.useMemo)(
4137
+ const visiblePageIndexes = (0, import_react6.useMemo)(
3801
4138
  () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
3802
4139
  [form.pageVisibility, pages]
3803
4140
  );
3804
4141
  const activePage = pages?.[currentPageIndex];
3805
4142
  const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
3806
4143
  const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
3807
- const visibleValues = (0, import_react5.useMemo)(() => (0, import_core4.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
3808
- 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)(
3809
4146
  () => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
3810
4147
  [form.schema, form.translate, form.values, form.visibility]
3811
4148
  );
3812
- const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
3813
- 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;
3814
4151
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3815
4152
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3816
4153
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3817
- const resolveMessage = (0, import_react5.useCallback)(
4154
+ const resolveMessage = (0, import_react6.useCallback)(
3818
4155
  (key, fallback) => {
3819
4156
  const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3820
4157
  const configured = messages[key];
@@ -3822,15 +4159,15 @@ function ContextFormRenderer({
3822
4159
  },
3823
4160
  [form.locale, messageResolver, messages]
3824
4161
  );
3825
- const fieldTranslate = (0, import_react5.useCallback)(
4162
+ const fieldTranslate = (0, import_react6.useCallback)(
3826
4163
  (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3827
4164
  [form.translate, messageResolver, messages.requiredField, resolveMessage]
3828
4165
  );
3829
- const focusSubmitButton = (0, import_react5.useCallback)(() => {
4166
+ const focusSubmitButton = (0, import_react6.useCallback)(() => {
3830
4167
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3831
4168
  button?.focus();
3832
4169
  }, []);
3833
- (0, import_react5.useEffect)(() => {
4170
+ (0, import_react6.useEffect)(() => {
3834
4171
  let active = true;
3835
4172
  if (receiptStore === void 0) {
3836
4173
  setReceipt(null);
@@ -3851,14 +4188,14 @@ function ContextFormRenderer({
3851
4188
  active = false;
3852
4189
  };
3853
4190
  }, [form.schema.id, form.schema.version, receiptStore]);
3854
- (0, import_react5.useEffect)(() => {
4191
+ (0, import_react6.useEffect)(() => {
3855
4192
  if (pages === void 0 || visiblePageIndexes.length === 0) {
3856
4193
  setCurrentPageIndex(0);
3857
4194
  return;
3858
4195
  }
3859
4196
  if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
3860
4197
  }, [currentPageIndex, pages, visiblePageIndexes]);
3861
- (0, import_react5.useEffect)(() => {
4198
+ (0, import_react6.useEffect)(() => {
3862
4199
  if (focusFieldId === null) return;
3863
4200
  const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
3864
4201
  (element) => element.dataset.fieldId === focusFieldId
@@ -3870,11 +4207,11 @@ function ContextFormRenderer({
3870
4207
  setFocusFieldId(null);
3871
4208
  }
3872
4209
  }, [focusFieldId]);
3873
- (0, import_react5.useEffect)(() => {
4210
+ (0, import_react6.useEffect)(() => {
3874
4211
  if (!isReplaceMode || form.submitStatus !== "success") return;
3875
4212
  completionRef.current?.focus();
3876
4213
  }, [form.submitStatus, isReplaceMode]);
3877
- (0, import_react5.useEffect)(() => {
4214
+ (0, import_react6.useEffect)(() => {
3878
4215
  if (confirmation === null) return;
3879
4216
  const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
3880
4217
  confirmButton?.focus();
@@ -3906,7 +4243,7 @@ function ContextFormRenderer({
3906
4243
  globalThis.addEventListener("keydown", onKeyDown);
3907
4244
  return () => globalThis.removeEventListener("keydown", onKeyDown);
3908
4245
  }, [confirmation, confirmationRenderMode, focusSubmitButton]);
3909
- (0, import_react5.useEffect)(() => {
4246
+ (0, import_react6.useEffect)(() => {
3910
4247
  if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
3911
4248
  const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
3912
4249
  if (loadedDraftKey.current === loadIdentity) return;
@@ -3918,7 +4255,7 @@ function ContextFormRenderer({
3918
4255
  form.restoreValues(draft.values);
3919
4256
  setDraftRestored(true);
3920
4257
  }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
3921
- (0, import_react5.useEffect)(() => {
4258
+ (0, import_react6.useEffect)(() => {
3922
4259
  if (form.submitStatus === "success") return;
3923
4260
  const timeout = globalThis.setTimeout(() => {
3924
4261
  onDraftSave?.(form.values);
@@ -3974,7 +4311,7 @@ function ContextFormRenderer({
3974
4311
  if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
3975
4312
  return { status: "cancelled" };
3976
4313
  }
3977
- const validation = (0, import_core4.validateAnswers)(form.schema, form.values);
4314
+ const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
3978
4315
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
3979
4316
  if (validation.valid && !guardsConfirmed) {
3980
4317
  rendererSubmissionInFlight.current = true;
@@ -4276,7 +4613,7 @@ function ContextFormRenderer({
4276
4613
  ...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
4277
4614
  };
4278
4615
  if (slots.renderField !== void 0) {
4279
- 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({
4280
4617
  question: field,
4281
4618
  value: form.values[field.id],
4282
4619
  onChange: (value) => {
@@ -4446,5 +4783,6 @@ function FormRenderer(props) {
4446
4783
  useField,
4447
4784
  useForm,
4448
4785
  useFormBuilder,
4449
- useSubmissionReceipts
4786
+ useSubmissionReceipts,
4787
+ useTranslationWorkspace
4450
4788
  });