@form-engine-ts/react 2.9.6 → 3.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/dist/index.js CHANGED
@@ -99,6 +99,7 @@ function createLocalStorageSubmissionAttemptStore(options = {}) {
99
99
 
100
100
  // src/builder.tsx
101
101
  import {
102
+ DEFAULT_FIELD_TYPE_DEFINITIONS,
102
103
  populateSchemaTranslations
103
104
  } from "@form-engine-ts/core";
104
105
  import { Children, createContext, isValidElement, useContext, useState } from "react";
@@ -743,6 +744,33 @@ function useFormBuilder({
743
744
  };
744
745
  }
745
746
 
747
+ // src/i18n.ts
748
+ var BUILDER_TRANSLATION_KEYS = {
749
+ ADD_FIELD: "builder.actions.addField",
750
+ SELECT_FIELD_TYPE: "builder.fields.selectType",
751
+ SELECT_LOCALE_TO_ADD: "builder.localization.selectLocaleToAdd",
752
+ FIELD_TYPE_TEXT: "builder.fields.typeText",
753
+ FIELD_TYPE_TEXTAREA: "builder.fields.typeTextarea",
754
+ FIELD_TYPE_NUMBER: "builder.fields.typeNumber",
755
+ FIELD_TYPE_RATING: "builder.fields.typeRating",
756
+ FIELD_TYPE_RADIO: "builder.fields.typeRadio",
757
+ FIELD_TYPE_CHECKBOX: "builder.fields.typeCheckbox",
758
+ FIELD_TYPE_SELECT: "builder.fields.typeSelect",
759
+ FIELD_TYPE_MULTI_SELECT: "builder.fields.typeMultiSelect"
760
+ };
761
+ var BUILDER_TRANSLATION_ALIASES = {
762
+ "builder.actions.addField": "builder.addQuestion",
763
+ "builder.localization.selectLocaleToAdd": "builder.selectLocaleToAdd",
764
+ "builder.fields.typeText": "builder.fieldType.text",
765
+ "builder.fields.typeTextarea": "builder.fieldType.textarea",
766
+ "builder.fields.typeNumber": "builder.fieldType.number",
767
+ "builder.fields.typeRating": "builder.fieldType.rating",
768
+ "builder.fields.typeRadio": "builder.fieldType.radio",
769
+ "builder.fields.typeCheckbox": "builder.fieldType.checkbox",
770
+ "builder.fields.typeSelect": "builder.fieldType.select",
771
+ "builder.fields.typeMultiSelect": "builder.fieldType.multi-select"
772
+ };
773
+
746
774
  // src/builder.tsx
747
775
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
748
776
  function DefaultButton({
@@ -813,6 +841,26 @@ function defaultIconFor(actionType) {
813
841
  return "\u6587";
814
842
  }
815
843
  }
844
+ function defaultFieldTypeIcon(type) {
845
+ switch (type) {
846
+ case "text":
847
+ return "T";
848
+ case "textarea":
849
+ return "\u2261";
850
+ case "number":
851
+ return "#";
852
+ case "rating":
853
+ return "\u2605";
854
+ case "select":
855
+ return "\u25BE";
856
+ case "multi-select":
857
+ return "\u2611";
858
+ case "checkbox":
859
+ return "\u25A1";
860
+ case "radio":
861
+ return "\u25C9";
862
+ }
863
+ }
816
864
  function DefaultTextInput({
817
865
  id,
818
866
  className,
@@ -925,8 +973,15 @@ function DefaultSelect({
925
973
  helperText,
926
974
  value,
927
975
  onChange,
928
- options
976
+ onKeyDown,
977
+ options,
978
+ renderOption,
979
+ renderValue
929
980
  }) {
981
+ const normalizedOptions = options.map(
982
+ (option) => typeof option === "string" ? { value: option, label: option } : option
983
+ );
984
+ const selectedOption = normalizedOptions.find((option) => option.value === value);
930
985
  const labelElement = label === void 0 ? null : id === void 0 ? /* @__PURE__ */ jsx("span", { children: label }) : /* @__PURE__ */ jsx("label", { htmlFor: id, children: label });
931
986
  return /* @__PURE__ */ jsxs(Fragment, { children: [
932
987
  labelElement,
@@ -944,9 +999,11 @@ function DefaultSelect({
944
999
  "aria-invalid": error === true ? true : void 0,
945
1000
  value,
946
1001
  onChange: (event) => onChange(event.currentTarget.value),
947
- children: options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))
1002
+ onKeyDown,
1003
+ children: normalizedOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, disabled: option.disabled, children: renderOption === void 0 ? option.label : renderOption(option) }, option.value))
948
1004
  }
949
1005
  ),
1006
+ renderValue === void 0 ? null : /* @__PURE__ */ jsx("output", { children: renderValue(selectedOption) }),
950
1007
  helperText === void 0 || helperText.length === 0 ? null : /* @__PURE__ */ jsx("small", { id: ariaDescribedBy, children: helperText })
951
1008
  ] });
952
1009
  }
@@ -1034,7 +1091,8 @@ var DEFAULT_COMPONENTS = {
1034
1091
  Section: DefaultSection,
1035
1092
  Fieldset: DefaultFieldset,
1036
1093
  ErrorMessage: DefaultErrorMessage,
1037
- renderIcon: defaultIconFor
1094
+ renderIcon: defaultIconFor,
1095
+ renderFieldTypeIcon: defaultFieldTypeIcon
1038
1096
  };
1039
1097
  var BuilderPrimitiveContext = createContext({
1040
1098
  components: DEFAULT_COMPONENTS,
@@ -1214,6 +1272,11 @@ var BUILDER_DEFAULTS = {
1214
1272
  "builder.translating": "Translating\u2026",
1215
1273
  "builder.translationLocale": "Translation locale",
1216
1274
  "builder.selectLocale": "Select a locale to edit translations.",
1275
+ "builder.localization.selectLocaleToAdd": "Select a locale to add",
1276
+ "builder.localization.noLocalesConfigured": "Translations not configured",
1277
+ "builder.localization.localesConfiguredSummary": "{{count}} locales configured",
1278
+ "builder.localization.allLocalesAdded": "\u3059\u3079\u3066\u306E\u5019\u88DC\u8A00\u8A9E\u304C\u8FFD\u52A0\u6E08\u307F\u3067\u3059",
1279
+ "builder.localization.maxLocalesReached": "\u767B\u9332\u53EF\u80FD\u306A\u6700\u5927\u8A00\u8A9E\u6570\uFF08{{max}}\uFF09\u306B\u9054\u3057\u307E\u3057\u305F",
1217
1280
  "builder.translation": "{{locale}} translation",
1218
1281
  "builder.translatedFormTitle": "Translated form title",
1219
1282
  "builder.translatedFormDescription": "Translated form description",
@@ -1230,6 +1293,16 @@ var BUILDER_DEFAULTS = {
1230
1293
  "builder.actions.close": "Close",
1231
1294
  "builder.actions.dragHandle": "Reorder",
1232
1295
  "builder.translationUnavailable": "Provide an async translation adapter to enable automatic translation.",
1296
+ "builder.fields.selectType": "Select field type",
1297
+ "builder.fields.typeText": "Text",
1298
+ "builder.fields.typeTextarea": "Textarea",
1299
+ "builder.fields.typeNumber": "Number",
1300
+ "builder.fields.typeRating": "Rating",
1301
+ "builder.fields.typeSelect": "Select",
1302
+ "builder.fields.typeMultiSelect": "Multi-select",
1303
+ "builder.fields.typeCheckbox": "Checkbox",
1304
+ "builder.fields.typeRadio": "Radio",
1305
+ "builder.actions.addField": "Add question",
1233
1306
  "builder.fieldType.text": "Text",
1234
1307
  "builder.fieldType.textarea": "Textarea",
1235
1308
  "builder.fieldType.number": "Number",
@@ -1266,7 +1339,7 @@ function OrderedBuilderSections({
1266
1339
  return groups.map((group, index) => ({ group, index, rank: ranks.get(sectionGroupName(group) ?? "questions") ?? order.length })).sort((left, right) => left.rank - right.rank || left.index - right.index).map(({ group }) => group);
1267
1340
  }
1268
1341
  function fieldTypeKey(type) {
1269
- return `builder.fieldType.${type}`;
1342
+ return DEFAULT_FIELD_TYPE_DEFINITIONS.find((definition) => definition.type === type)?.labelKey ?? `builder.fieldType.${type}`;
1270
1343
  }
1271
1344
  function operatorKey(operator) {
1272
1345
  return `builder.operator.${operator}`;
@@ -1427,7 +1500,8 @@ function FormBuilder({
1427
1500
  const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
1428
1501
  const components = {
1429
1502
  ...GUARDED_COMPONENTS,
1430
- renderIcon: resolvedComponents.renderIcon
1503
+ renderIcon: resolvedComponents.renderIcon,
1504
+ renderFieldTypeIcon: resolvedComponents.renderFieldTypeIcon
1431
1505
  };
1432
1506
  const defaultStylesDisabled = disableDefaultStyles || unstyled;
1433
1507
  const builderClass = (value) => defaultStylesDisabled ? void 0 : value;
@@ -1458,6 +1532,12 @@ function FormBuilder({
1458
1532
  if (typeof value === "string" || typeof value === "number") translatorParams[name] = value;
1459
1533
  }
1460
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
+ }
1461
1541
  return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
1462
1542
  };
1463
1543
  const pagesEnabled = features?.pages ?? true;
@@ -2193,6 +2273,7 @@ function FormBuilder({
2193
2273
  index,
2194
2274
  currentLocale: editingLocale,
2195
2275
  translate,
2276
+ ...slots === void 0 ? {} : { slots },
2196
2277
  ...policy === void 0 ? {} : { policy },
2197
2278
  ...features === void 0 ? {} : { features },
2198
2279
  readOnly,
@@ -2564,7 +2645,7 @@ function FormBuilder({
2564
2645
  action: "addField",
2565
2646
  disabled: initialFieldType === null || maxFieldsReached,
2566
2647
  onClick: addField,
2567
- children: translate("builder.addQuestion")
2648
+ children: translate(BUILDER_TRANSLATION_KEYS.ADD_FIELD)
2568
2649
  }
2569
2650
  ) })
2570
2651
  ] }) })
@@ -2585,6 +2666,18 @@ import {
2585
2666
  validatePageAnswers
2586
2667
  } from "@form-engine-ts/core";
2587
2668
  import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect, useMemo as useMemo2, useRef, useState as useState2 } from "react";
2669
+
2670
+ // src/types.ts
2671
+ var FormSubmissionError = class extends Error {
2672
+ payload;
2673
+ constructor(message, payload) {
2674
+ super(message);
2675
+ this.name = "FormSubmissionError";
2676
+ this.payload = payload ?? { formError: message };
2677
+ }
2678
+ };
2679
+
2680
+ // src/context.tsx
2588
2681
  import { jsx as jsx2 } from "react/jsx-runtime";
2589
2682
  var FormContext = createContext2(null);
2590
2683
  function issuesByField(issues) {
@@ -2592,6 +2685,18 @@ function issuesByField(issues) {
2592
2685
  for (const issue of issues) result[issue.fieldId] ??= issue;
2593
2686
  return result;
2594
2687
  }
2688
+ function defaultAttemptId2() {
2689
+ const randomUuid = globalThis.crypto?.randomUUID;
2690
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
2691
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
2692
+ }
2693
+ function isServerErrorPayload(value) {
2694
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2695
+ const record = value;
2696
+ const fieldErrors = record.fieldErrors;
2697
+ const formError = record.formError;
2698
+ return (Object.hasOwn(record, "fieldErrors") || Object.hasOwn(record, "formError")) && (fieldErrors === void 0 || typeof fieldErrors === "object" && fieldErrors !== null && !Array.isArray(fieldErrors) && Object.values(fieldErrors).every((message) => typeof message === "string")) && (formError === void 0 || typeof formError === "string");
2699
+ }
2595
2700
  function FormProvider({
2596
2701
  schema,
2597
2702
  locale,
@@ -2676,7 +2781,7 @@ function FormProvider({
2676
2781
  setSubmitError(null);
2677
2782
  }, [initialValues]);
2678
2783
  const submit = useCallback2(
2679
- async (beforeSubmit, prepareSubmission) => {
2784
+ async (beforeSubmit, submitContext) => {
2680
2785
  if (submissionInFlight.current) return { status: "cancelled" };
2681
2786
  const validation = validateAnswers(validSchema, values);
2682
2787
  if (!validation.valid) {
@@ -2697,13 +2802,22 @@ function FormProvider({
2697
2802
  setSubmitStatus("idle");
2698
2803
  return { status: "cancelled" };
2699
2804
  }
2700
- const submissionValues = prepareSubmission === void 0 ? visibleValues : await prepareSubmission(visibleValues);
2701
- const response = await onSubmit(submissionValues);
2805
+ const context = submitContext ?? {
2806
+ attemptId: defaultAttemptId2(),
2807
+ formId: validSchema.id,
2808
+ formVersion: validSchema.version,
2809
+ locale,
2810
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString()
2811
+ };
2812
+ const response = await onSubmit({ ...visibleValues }, context);
2702
2813
  if (resetOnSuccess) setValues({ ...initialValues });
2703
2814
  setSubmitStatus("success");
2704
2815
  return response === void 0 ? { status: "success" } : { status: "success", response };
2705
2816
  } catch (cause) {
2706
- const error = cause instanceof Error ? cause : new Error(String(cause));
2817
+ const error = isServerErrorPayload(cause) ? new FormSubmissionError(
2818
+ cause.formError ?? (cause instanceof Error ? cause.message : "Form submission failed."),
2819
+ cause
2820
+ ) : cause instanceof Error ? cause : new Error(String(cause));
2707
2821
  setSubmitError(error);
2708
2822
  setSubmitStatus("error");
2709
2823
  return { status: "error", error };
@@ -2711,7 +2825,7 @@ function FormProvider({
2711
2825
  submissionInFlight.current = false;
2712
2826
  }
2713
2827
  },
2714
- [initialValues, onSubmit, resetOnSuccess, validSchema, values]
2828
+ [initialValues, locale, onSubmit, resetOnSuccess, validSchema, values]
2715
2829
  );
2716
2830
  const translate = useCallback2(
2717
2831
  (key, params) => translator.translate(key, locale, params),
@@ -2902,18 +3016,6 @@ import {
2902
3016
  useRef as useRef2,
2903
3017
  useState as useState4
2904
3018
  } from "react";
2905
-
2906
- // src/types.ts
2907
- var FormSubmissionError = class extends Error {
2908
- payload;
2909
- constructor(message, payload) {
2910
- super(message);
2911
- this.name = "FormSubmissionError";
2912
- this.payload = payload ?? { formError: message };
2913
- }
2914
- };
2915
-
2916
- // src/renderer.tsx
2917
3019
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2918
3020
  function describedBy(field, error, helpId, errorId) {
2919
3021
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
@@ -3100,7 +3202,11 @@ function DefaultField(props) {
3100
3202
  fieldId: field.id,
3101
3203
  current: typeof value === "string" ? value.length : 0,
3102
3204
  max: field.maxLength
3103
- }) : null,
3205
+ }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-character-count", "aria-live": "polite", children: [
3206
+ typeof value === "string" ? value.length : 0,
3207
+ " / ",
3208
+ field.maxLength
3209
+ ] }) : null,
3104
3210
  /* @__PURE__ */ jsx3(FieldMessage, { props })
3105
3211
  ] });
3106
3212
  }
@@ -3152,11 +3258,58 @@ function parseDraft(serialized) {
3152
3258
  return null;
3153
3259
  }
3154
3260
  }
3261
+ var DEFAULT_RENDERER_MESSAGES = {
3262
+ en: {
3263
+ submitButton: "Submit",
3264
+ submittingButton: "Submitting...",
3265
+ retryButton: "Retry",
3266
+ requiredField: "This field is required.",
3267
+ alreadySubmittedTitle: "Already Submitted",
3268
+ alreadySubmittedMessage: "Already submitted.",
3269
+ serverErrorSummary: "Submission failed. Please check your answers and try again.",
3270
+ confirmSensitiveDataTitle: "Sensitive data may be included",
3271
+ confirmSensitiveDataMessage: "The following answers may contain personal information. Continue submitting?",
3272
+ confirmButton: "Proceed",
3273
+ cancelButton: "Cancel"
3274
+ },
3275
+ ja: {
3276
+ submitButton: "\u9001\u4FE1\u3059\u308B",
3277
+ submittingButton: "\u9001\u4FE1\u4E2D...",
3278
+ retryButton: "\u518D\u9001\u4FE1\u3059\u308B",
3279
+ requiredField: "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059",
3280
+ alreadySubmittedTitle: "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3281
+ alreadySubmittedMessage: "\u3053\u306E\u30A2\u30F3\u30B1\u30FC\u30C8\u306B\u306F\u3059\u3067\u306B\u56DE\u7B54\u3057\u3066\u3044\u307E\u3059\u3002",
3282
+ serverErrorSummary: "\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u5185\u5BB9\u3092\u3054\u78BA\u8A8D\u306E\u4E0A\u3001\u518D\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002",
3283
+ confirmSensitiveDataTitle: "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059",
3284
+ confirmSensitiveDataMessage: "\u4EE5\u4E0B\u306E\u9805\u76EE\u306B\u500B\u4EBA\u60C5\u5831\u3068\u307F\u3089\u308C\u308B\u8A18\u8FF0\u304C\u3042\u308A\u307E\u3059\u3002\u3053\u306E\u307E\u307E\u9001\u4FE1\u3057\u3066\u3082\u3088\u308D\u3057\u3044\u3067\u3059\u304B\uFF1F",
3285
+ confirmButton: "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3286
+ cancelButton: "\u4FEE\u6B63\u3059\u308B"
3287
+ }
3288
+ };
3289
+ function createRendererAttemptId() {
3290
+ const randomUuid = globalThis.crypto?.randomUUID;
3291
+ if (typeof randomUuid === "function") return randomUuid.call(globalThis.crypto);
3292
+ return `attempt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
3293
+ }
3294
+ function maskSensitiveValue(finding) {
3295
+ if (finding.maskedText !== void 0) return finding.maskedText;
3296
+ const value = finding.matchedText;
3297
+ if (value === void 0) return void 0;
3298
+ if (finding.type === "email") {
3299
+ const separator = value.indexOf("@");
3300
+ if (separator > 0) return `${value.slice(0, Math.min(2, separator))}***${value.slice(separator)}`;
3301
+ }
3302
+ if (finding.type === "phone" || finding.type === "postal_code") return "***";
3303
+ return value.length <= 2 ? "***" : `${value.slice(0, 2)}***`;
3304
+ }
3155
3305
  function ContextFormRenderer({
3156
3306
  components = {},
3157
3307
  className = "",
3158
3308
  successMessageKey,
3159
3309
  errorMessageKey,
3310
+ attemptIdFactory,
3311
+ messages = {},
3312
+ messageResolver,
3160
3313
  autoSaveKey,
3161
3314
  beforeSubmit,
3162
3315
  onDraftSave,
@@ -3185,6 +3338,7 @@ function ContextFormRenderer({
3185
3338
  const [completionData, setCompletionData] = useState4(null);
3186
3339
  const [receiptLoaded, setReceiptLoaded] = useState4(receiptStore === void 0);
3187
3340
  const rendererSubmissionInFlight = useRef2(false);
3341
+ const fallbackAttemptId = useRef2(null);
3188
3342
  const completionRef = useRef2(null);
3189
3343
  const confirmationRef = useRef2(null);
3190
3344
  const pages = form.schema.pages;
@@ -3199,6 +3353,18 @@ function ContextFormRenderer({
3199
3353
  const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
3200
3354
  const interactionLocked = submitState === "confirming" || submitState === "submitting";
3201
3355
  const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
3356
+ const resolveMessage = useCallback3(
3357
+ (key, fallback) => {
3358
+ const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
3359
+ const configured = messages[key];
3360
+ return messageResolver?.(key, configured ?? defaultText) ?? configured ?? defaultText;
3361
+ },
3362
+ [form.locale, messageResolver, messages]
3363
+ );
3364
+ const fieldTranslate = useCallback3(
3365
+ (key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
3366
+ [form.translate, messageResolver, messages.requiredField, resolveMessage]
3367
+ );
3202
3368
  const focusSubmitButton = useCallback3(() => {
3203
3369
  const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
3204
3370
  button?.focus();
@@ -3238,6 +3404,7 @@ function ContextFormRenderer({
3238
3404
  );
3239
3405
  const control = fieldContainer?.querySelector("input, select, textarea");
3240
3406
  if (control !== void 0 && control !== null) {
3407
+ control.scrollIntoView?.({ behavior: "smooth", block: "center" });
3241
3408
  control.focus();
3242
3409
  setFocusFieldId(null);
3243
3410
  }
@@ -3256,6 +3423,23 @@ function ContextFormRenderer({
3256
3423
  event.preventDefault();
3257
3424
  setConfirmation(null);
3258
3425
  globalThis.setTimeout(focusSubmitButton, 0);
3426
+ return;
3427
+ }
3428
+ if (event.key !== "Tab") return;
3429
+ const focusable = [
3430
+ ...confirmationRef.current?.querySelectorAll(
3431
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
3432
+ ) ?? []
3433
+ ].filter((element) => !element.hasAttribute("disabled"));
3434
+ if (focusable.length === 0) return;
3435
+ const first = focusable[0];
3436
+ const last = focusable[focusable.length - 1];
3437
+ if (event.shiftKey && document.activeElement === first) {
3438
+ event.preventDefault();
3439
+ last?.focus();
3440
+ } else if (!event.shiftKey && document.activeElement === last) {
3441
+ event.preventDefault();
3442
+ first?.focus();
3259
3443
  }
3260
3444
  };
3261
3445
  globalThis.addEventListener("keydown", onKeyDown);
@@ -3357,17 +3541,24 @@ function ContextFormRenderer({
3357
3541
  rendererSubmissionInFlight.current = true;
3358
3542
  try {
3359
3543
  let submissionAttempt;
3360
- const result = await form.submit(
3361
- beforeSubmit,
3362
- attemptStore === void 0 ? void 0 : async (values) => {
3363
- submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version);
3364
- return {
3365
- ...values,
3366
- attemptId: submissionAttempt.attemptId,
3367
- submissionId: submissionAttempt.attemptId
3368
- };
3369
- }
3370
- );
3544
+ let attemptId = fallbackAttemptId.current;
3545
+ if (attemptStore !== void 0) {
3546
+ submissionAttempt = await attemptStore.getOrCreate(form.schema.id, form.schema.version, attemptIdFactory);
3547
+ attemptId = submissionAttempt.attemptId;
3548
+ } else if (attemptId === null) {
3549
+ attemptId = attemptIdFactory?.() ?? createRendererAttemptId();
3550
+ fallbackAttemptId.current = attemptId;
3551
+ }
3552
+ if (attemptId === null) throw new Error("Unable to create a submission attempt id.");
3553
+ const submittedAt = (/* @__PURE__ */ new Date()).toISOString();
3554
+ const submitContext = {
3555
+ attemptId,
3556
+ formId: form.schema.id,
3557
+ formVersion: form.schema.version,
3558
+ locale: form.locale,
3559
+ submittedAt
3560
+ };
3561
+ const result = await form.submit(beforeSubmit, submitContext);
3371
3562
  if (result.status === "invalid") {
3372
3563
  const invalidPageIndex = pages?.findIndex(
3373
3564
  (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
@@ -3380,7 +3571,7 @@ function ContextFormRenderer({
3380
3571
  if (result.error instanceof FormSubmissionError) {
3381
3572
  const fieldErrors = result.error.payload.fieldErrors ?? {};
3382
3573
  form.setServerErrors?.(fieldErrors);
3383
- const firstServerFieldId = Object.keys(fieldErrors)[0];
3574
+ const firstServerFieldId = form.schema.fields.find((field) => Object.hasOwn(fieldErrors, field.id))?.id ?? Object.keys(fieldErrors)[0];
3384
3575
  if (firstServerFieldId !== void 0) {
3385
3576
  const invalidPageIndex = pages?.findIndex((page) => page.questionIds.includes(firstServerFieldId));
3386
3577
  if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
@@ -3405,11 +3596,11 @@ function ContextFormRenderer({
3405
3596
  });
3406
3597
  if (receiptStore !== void 0) {
3407
3598
  const response = result.response;
3408
- const submissionId = response?.submissionId ?? submissionAttempt?.attemptId;
3599
+ const submissionId = response?.submissionId ?? submissionAttempt?.attemptId ?? attemptId;
3409
3600
  const storedReceipt = {
3410
3601
  formId: form.schema.id,
3411
3602
  formVersion: form.schema.version,
3412
- submittedAt: response?.submittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3603
+ submittedAt: response?.submittedAt ?? submittedAt,
3413
3604
  ...submissionId === void 0 ? {} : { submissionId }
3414
3605
  };
3415
3606
  try {
@@ -3428,6 +3619,7 @@ function ContextFormRenderer({
3428
3619
  } catch {
3429
3620
  }
3430
3621
  }
3622
+ if (attemptStore === void 0) fallbackAttemptId.current = null;
3431
3623
  if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
3432
3624
  globalThis.localStorage.removeItem(autoSaveKey);
3433
3625
  setDraftRestored(false);
@@ -3467,7 +3659,10 @@ function ContextFormRenderer({
3467
3659
  disabled: interactionLocked || submitState === "success",
3468
3660
  onSubmit: () => void submitValues()
3469
3661
  };
3470
- return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
3662
+ return slots.renderSubmitButton?.(submitButtonProps) ?? /* @__PURE__ */ jsxs2("button", { className: "fe-submit", type: "submit", disabled: submitButtonProps.disabled, children: [
3663
+ submitState === "submitting" ? /* @__PURE__ */ jsx3("span", { className: "fe-spinner", "aria-hidden": "true" }) : null,
3664
+ submitState === "submitting" ? resolveMessage("submittingButton") : resolveMessage("submitButton", form.translate(form.schema.submitLabelKey ?? "form.submit"))
3665
+ ] });
3471
3666
  };
3472
3667
  const completionMessage = form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey));
3473
3668
  const activeCompletionData = completionData ?? {
@@ -3500,15 +3695,31 @@ function ContextFormRenderer({
3500
3695
  role: submissionConfirmationRenderMode === "dialog" ? void 0 : "dialog",
3501
3696
  children: slots.renderSubmissionConfirmation?.({
3502
3697
  findings: confirmation?.findings ?? [],
3503
- message: confirmation?.message ?? form.translate("form.confirmSensitiveData"),
3698
+ message: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")),
3504
3699
  schema: form.schema,
3505
3700
  visibleValues,
3506
3701
  onConfirm: confirmSubmission,
3507
3702
  onCancel: cancelSubmission
3508
3703
  }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
3509
- /* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? form.translate("form.confirmSensitiveData") }),
3510
- /* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: form.translate("form.confirmSubmission") }),
3511
- /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: form.translate("form.cancelSubmission") })
3704
+ /* @__PURE__ */ jsx3("h2", { children: resolveMessage("confirmSensitiveDataTitle") }),
3705
+ /* @__PURE__ */ jsx3("p", { children: confirmation?.message ?? resolveMessage("confirmSensitiveDataMessage", form.translate("form.confirmSensitiveData")) }),
3706
+ (confirmation?.findings ?? []).length === 0 ? null : /* @__PURE__ */ jsx3("ul", { children: (confirmation?.findings ?? []).map((finding, index) => {
3707
+ const field = form.schema.fields.find((candidate) => candidate.id === finding.fieldId);
3708
+ const typeLabels = form.locale.toLowerCase().startsWith("ja") ? { email: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9", phone: "\u96FB\u8A71\u756A\u53F7", url: "URL", postal_code: "\u90F5\u4FBF\u756A\u53F7" } : { email: "Email address", phone: "Phone number", url: "URL", postal_code: "Postal code" };
3709
+ const typeLabel = finding.typeLabel ?? typeLabels[finding.type] ?? finding.type;
3710
+ const value = maskSensitiveValue(finding);
3711
+ return /* @__PURE__ */ jsxs2("li", { children: [
3712
+ /* @__PURE__ */ jsx3("span", { children: finding.fieldTitle ?? field?.title ?? finding.fieldId }),
3713
+ " ",
3714
+ /* @__PURE__ */ jsx3("span", { className: "fe-sensitive-type", children: typeLabel }),
3715
+ value === void 0 ? null : /* @__PURE__ */ jsxs2("span", { className: "fe-sensitive-value", children: [
3716
+ " ",
3717
+ value
3718
+ ] })
3719
+ ] }, `${finding.fieldId}-${finding.type}-${finding.start ?? index}`);
3720
+ }) }),
3721
+ /* @__PURE__ */ jsx3("button", { type: "button", "data-fe-confirm": "true", onClick: confirmSubmission, children: resolveMessage("confirmButton", form.translate("form.confirmSubmission")) }),
3722
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: cancelSubmission, children: resolveMessage("cancelButton", form.translate("form.cancelSubmission")) })
3512
3723
  ] })
3513
3724
  }
3514
3725
  );
@@ -3518,8 +3729,9 @@ function ContextFormRenderer({
3518
3729
  receipt,
3519
3730
  ...receiptStore === void 0 ? {} : { onReset: () => void resetReceipt() }
3520
3731
  }) ?? /* @__PURE__ */ jsxs2("div", { role: "status", children: [
3521
- form.translate("form.alreadySubmitted"),
3522
- receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: form.translate("form.submitAnother") })
3732
+ /* @__PURE__ */ jsx3("h2", { children: resolveMessage("alreadySubmittedTitle") }),
3733
+ /* @__PURE__ */ jsx3("p", { children: resolveMessage("alreadySubmittedMessage", form.translate("form.alreadySubmitted")) }),
3734
+ receiptStore === void 0 ? null : /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void resetReceipt(), children: resolveMessage("submitButton", form.translate("form.submitAnother")) })
3523
3735
  ] }) });
3524
3736
  }
3525
3737
  if (form.submitStatus === "success" && isReplaceMode) {
@@ -3582,7 +3794,7 @@ function ContextFormRenderer({
3582
3794
  value: form.values[field.id],
3583
3795
  error,
3584
3796
  setValue: (value) => form.setValue(field.id, value),
3585
- translate: form.translate,
3797
+ translate: fieldTranslate,
3586
3798
  inputId: `${prefix}-${field.id}`,
3587
3799
  errorId: `${prefix}-${field.id}-error`,
3588
3800
  helpId: `${prefix}-${field.id}-help`,
@@ -3649,7 +3861,13 @@ function ContextFormRenderer({
3649
3861
  ] }),
3650
3862
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
3651
3863
  form.submitStatus === "success" ? completionRegion : null,
3652
- form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.submitError.payload.formError }) : errorMessageKey === void 0 ? null : /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) })) : null
3864
+ form.submitStatus === "error" && form.submitError !== null ? slots.renderSubmitError?.({ error: form.submitError, onRetry: () => void submitValues() }) ?? (form.submitError instanceof FormSubmissionError && form.submitError.payload.formError !== void 0 ? /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
3865
+ form.submitError.payload.formError,
3866
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3867
+ ] }) : /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
3868
+ errorMessageKey === void 0 ? resolveMessage("serverErrorSummary") : form.translate(errorMessageKey),
3869
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => void submitValues(), children: resolveMessage("retryButton") })
3870
+ ] })) : null
3653
3871
  ] })
3654
3872
  ]
3655
3873
  }
@@ -3665,7 +3883,7 @@ var RENDERER_MESSAGES = {
3665
3883
  "form.draftRestored": "Draft restored",
3666
3884
  "form.submissionBlocked": "Submission blocked because sensitive data was detected.",
3667
3885
  "form.confirmSensitiveData": "Sensitive data may be included. Confirm before submitting.",
3668
- "form.confirmSubmission": "Confirm submission",
3886
+ "form.confirmSubmission": "Proceed",
3669
3887
  "form.cancelSubmission": "Cancel",
3670
3888
  "form.yes": "Yes",
3671
3889
  "form.no": "No",
@@ -3673,9 +3891,26 @@ var RENDERER_MESSAGES = {
3673
3891
  "form.submitAnother": "Submit another response",
3674
3892
  "validation.required": "This field is required."
3675
3893
  };
3894
+ var RENDERER_MESSAGES_JA = {
3895
+ "form.submit": "\u9001\u4FE1\u3059\u308B",
3896
+ "form.back": "\u623B\u308B",
3897
+ "form.next": "\u6B21\u3078",
3898
+ "form.step": "{{current}} / {{total}}",
3899
+ "form.draftRestored": "\u4E0B\u66F8\u304D\u3092\u5FA9\u5143\u3057\u307E\u3057\u305F",
3900
+ "form.submissionBlocked": "\u500B\u4EBA\u60C5\u5831\u304C\u691C\u51FA\u3055\u308C\u305F\u305F\u3081\u9001\u4FE1\u3067\u304D\u307E\u305B\u3093\u3002",
3901
+ "form.confirmSensitiveData": "\u500B\u4EBA\u60C5\u5831\u304C\u542B\u307E\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u9001\u4FE1\u524D\u306B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
3902
+ "form.confirmSubmission": "\u3053\u306E\u307E\u307E\u9001\u4FE1",
3903
+ "form.cancelSubmission": "\u4FEE\u6B63\u3059\u308B",
3904
+ "form.yes": "\u306F\u3044",
3905
+ "form.no": "\u3044\u3044\u3048",
3906
+ "form.alreadySubmitted": "\u56DE\u7B54\u6E08\u307F\u3067\u3059",
3907
+ "form.submitAnother": "\u5225\u306E\u56DE\u7B54\u3092\u9001\u4FE1",
3908
+ "validation.required": "\u3053\u306E\u9805\u76EE\u306F\u5FC5\u9808\u3067\u3059"
3909
+ };
3676
3910
  var defaultRendererTranslator = {
3677
- translate(key, _locale, params = {}) {
3678
- return (RENDERER_MESSAGES[key] ?? key).replace(
3911
+ translate(key, locale, params = {}) {
3912
+ const localizedMessages = locale.toLowerCase().startsWith("ja") ? RENDERER_MESSAGES_JA : RENDERER_MESSAGES;
3913
+ return (localizedMessages[key] ?? key).replace(
3679
3914
  /\{\{(\w+)\}\}/g,
3680
3915
  (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
3681
3916
  );
@@ -3706,6 +3941,8 @@ function FormRenderer(props) {
3706
3941
  );
3707
3942
  }
3708
3943
  export {
3944
+ BUILDER_TRANSLATION_ALIASES,
3945
+ BUILDER_TRANSLATION_KEYS,
3709
3946
  FormBuilder,
3710
3947
  FormProvider,
3711
3948
  FormRenderer,
package/dist/styles.css CHANGED
@@ -93,6 +93,27 @@
93
93
  font-size: 0.875rem;
94
94
  font-weight: 600;
95
95
  }
96
+ .fe-character-count {
97
+ color: #667085;
98
+ font-size: 0.8rem;
99
+ text-align: right;
100
+ }
101
+ .fe-spinner {
102
+ display: inline-block;
103
+ width: 0.85em;
104
+ height: 0.85em;
105
+ margin-right: 0.4em;
106
+ border: 0.12em solid currentColor;
107
+ border-right-color: transparent;
108
+ border-radius: 50%;
109
+ animation: fe-spin 0.8s linear infinite;
110
+ vertical-align: -0.1em;
111
+ }
112
+ @keyframes fe-spin {
113
+ to {
114
+ transform: rotate(360deg);
115
+ }
116
+ }
96
117
  .fe-check-label {
97
118
  align-items: flex-start;
98
119
  display: flex;
@@ -298,3 +319,16 @@
298
319
  border-radius: 0.5rem;
299
320
  box-shadow: 0 1rem 3rem rgb(0 0 0 / 25%);
300
321
  }
322
+ .fe-sensitive-type {
323
+ border-radius: 999px;
324
+ background: #f2f4f7;
325
+ padding: 0.15rem 0.45rem;
326
+ font-size: 0.8rem;
327
+ }
328
+ .fe-sensitive-value {
329
+ font-family:
330
+ ui-monospace,
331
+ SFMono-Regular,
332
+ Menlo,
333
+ monospace;
334
+ }