@form-engine-ts/react 1.0.0 → 2.0.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
@@ -1,5 +1,387 @@
1
1
  // src/builder.tsx
2
- import { sanitizeSchema } from "@form-engine-ts/core";
2
+ import {
3
+ populateSchemaTranslations,
4
+ sanitizeSchema
5
+ } from "@form-engine-ts/core";
6
+ import { useState } from "react";
7
+
8
+ // src/hooks/useFormBuilder.ts
9
+ import {
10
+ validateFormSchema
11
+ } from "@form-engine-ts/core";
12
+ import { useCallback, useMemo } from "react";
13
+ var DEFAULT_PREFIXES = { field: "q", option: "opt", page: "page" };
14
+ function defaultIdFactory(kind, existingIds) {
15
+ const prefix = DEFAULT_PREFIXES[kind];
16
+ let id;
17
+ do {
18
+ id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
19
+ } while (existingIds.has(id));
20
+ return id;
21
+ }
22
+ function newField(type, id, optionId) {
23
+ const base = { id, title: "New question", required: false };
24
+ if (type === "text" || type === "textarea") return { ...base, type };
25
+ if (type === "number") return { ...base, type };
26
+ if (type === "rating") return { ...base, type, min: 1, max: 5 };
27
+ if (type === "checkbox") return { ...base, type };
28
+ return optionId === void 0 ? void 0 : { ...base, type, options: [{ id: optionId, label: "Option 1" }] };
29
+ }
30
+ function move(items, sourceIndex, targetIndex) {
31
+ if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
32
+ return void 0;
33
+ const result = [...items];
34
+ const [item] = result.splice(sourceIndex, 1);
35
+ if (item === void 0) return void 0;
36
+ result.splice(targetIndex, 0, item);
37
+ return result;
38
+ }
39
+ function addPolicyIssues(schema, policy, issues) {
40
+ if (policy?.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
41
+ issues.push({
42
+ path: "fields",
43
+ code: "max_fields_exceeded",
44
+ message: `At most ${policy.maxFields} fields are allowed.`
45
+ });
46
+ }
47
+ schema.fields.forEach((field, index) => {
48
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
49
+ issues.push({
50
+ path: `fields[${index}].type`,
51
+ code: "disallowed_field_type",
52
+ message: `Field type ${field.type} is not allowed.`
53
+ });
54
+ }
55
+ if (policy?.maxTextLength !== void 0) {
56
+ for (const [property, text] of [
57
+ ["title", field.title],
58
+ ["description", field.description]
59
+ ]) {
60
+ if (text !== void 0 && text.length > policy.maxTextLength) {
61
+ issues.push({
62
+ path: `fields[${index}].${property}`,
63
+ code: "max_text_length_exceeded",
64
+ message: `Text must be at most ${policy.maxTextLength} characters.`
65
+ });
66
+ }
67
+ }
68
+ }
69
+ if (policy?.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
70
+ issues.push({
71
+ path: `fields[${index}].options`,
72
+ code: "max_options_exceeded",
73
+ message: `At most ${policy.maxOptionsPerField} options are allowed.`
74
+ });
75
+ }
76
+ });
77
+ for (const locale of policy?.requiredLocales ?? []) {
78
+ if (!(schema.supportedLocales ?? []).includes(locale)) {
79
+ issues.push({
80
+ path: "supportedLocales",
81
+ code: "required_locale_missing",
82
+ message: `Required locale ${locale} is missing.`
83
+ });
84
+ }
85
+ if (locale === schema.defaultLocale) continue;
86
+ const requiredTranslations = [
87
+ { path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title },
88
+ ...schema.fields.map((field, index) => ({
89
+ path: `fields[${index}].translations.${locale}.title`,
90
+ value: field.translations?.[locale]?.title
91
+ })),
92
+ ...schema.fields.flatMap(
93
+ (field, fieldIndex) => "options" in field ? field.options.map((option, optionIndex) => ({
94
+ path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
95
+ value: option.translations?.[locale]
96
+ })) : []
97
+ ),
98
+ ...schema.pages?.flatMap(
99
+ (page, pageIndex) => page.title === void 0 ? [] : [{ path: `pages[${pageIndex}].translations.${locale}.title`, value: page.translations?.[locale]?.title }]
100
+ ) ?? []
101
+ ];
102
+ for (const translation of requiredTranslations) {
103
+ if (translation.value === void 0 || translation.value.trim().length === 0) {
104
+ issues.push({
105
+ path: translation.path,
106
+ code: "required_translation_missing",
107
+ message: `A translation for required locale ${locale} is missing.`
108
+ });
109
+ }
110
+ }
111
+ }
112
+ }
113
+ function useFormBuilder({
114
+ schema,
115
+ onChange,
116
+ policy,
117
+ idFactory = defaultIdFactory
118
+ }) {
119
+ const createId = useCallback(
120
+ (kind, existingIds) => {
121
+ const id = idFactory(kind, existingIds).trim();
122
+ return id.length > 0 && !existingIds.has(id) ? id : void 0;
123
+ },
124
+ [idFactory]
125
+ );
126
+ const addField = useCallback(
127
+ (type, pageId) => {
128
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type)) {
129
+ return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
130
+ }
131
+ if (policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields) {
132
+ return { success: false, error: { type: "max_fields_exceeded", max: policy.maxFields } };
133
+ }
134
+ const fieldId = createId("field", new Set(schema.fields.map((field2) => field2.id)));
135
+ const needsOption = ["select", "radio", "multi-select"].includes(type);
136
+ const optionId = needsOption ? createId(
137
+ "option",
138
+ new Set(
139
+ schema.fields.flatMap((field2) => "options" in field2 ? field2.options.map((option) => option.id) : [])
140
+ )
141
+ ) : void 0;
142
+ if (fieldId === void 0 || needsOption && optionId === void 0) {
143
+ return {
144
+ success: false,
145
+ error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
146
+ };
147
+ }
148
+ const field = newField(type, fieldId, optionId);
149
+ if (field === void 0) {
150
+ return { success: false, error: { type: "invalid_operation", message: "Could not create the field." } };
151
+ }
152
+ const pages = schema.pages?.map((page, index) => ({
153
+ ...page,
154
+ questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, fieldId] : page.questionIds
155
+ }));
156
+ if (schema.pages !== void 0 && !schema.pages.some((page) => page.id === pageId) && pageId !== void 0) {
157
+ return { success: false, error: { type: "invalid_operation", message: `Unknown page: ${pageId}` } };
158
+ }
159
+ onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
160
+ return { success: true };
161
+ },
162
+ [createId, onChange, policy, schema]
163
+ );
164
+ const removeField = useCallback(
165
+ (fieldId) => {
166
+ if (schema.fields.length <= 1 || !schema.fields.some((field) => field.id === fieldId)) return;
167
+ const fields = schema.fields.filter((field) => field.id !== fieldId).map(
168
+ (field) => field.displayCondition?.questionId === fieldId ? (({ displayCondition: _condition, ...candidate }) => candidate)(field) : field
169
+ );
170
+ const remainingPages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
171
+ if (schema.pages !== void 0 && remainingPages?.length === 0) {
172
+ const { pages: _pages, ...singlePageSchema } = schema;
173
+ onChange({ ...singlePageSchema, fields });
174
+ } else {
175
+ onChange({ ...schema, fields, ...remainingPages === void 0 ? {} : { pages: remainingPages } });
176
+ }
177
+ },
178
+ [onChange, schema]
179
+ );
180
+ const moveField = useCallback(
181
+ (fieldId, targetIndex) => {
182
+ const fields = move(
183
+ schema.fields,
184
+ schema.fields.findIndex((field) => field.id === fieldId),
185
+ targetIndex
186
+ );
187
+ if (fields !== void 0) {
188
+ const indexById = new Map(fields.map((field, index) => [field.id, index]));
189
+ const safeFields = fields.map((field, index) => {
190
+ const sourceIndex = field.displayCondition === void 0 ? void 0 : indexById.get(field.displayCondition.questionId);
191
+ if (field.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < index) return field;
192
+ const { displayCondition: _condition, ...withoutCondition } = field;
193
+ return withoutCondition;
194
+ });
195
+ onChange({ ...schema, fields: safeFields });
196
+ }
197
+ },
198
+ [onChange, schema]
199
+ );
200
+ const updateField = useCallback(
201
+ (fieldId, updater) => {
202
+ const current = schema.fields.find((field) => field.id === fieldId);
203
+ if (current === void 0) return;
204
+ const updated = updater(current);
205
+ if (updated.id !== fieldId) return;
206
+ if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type)) return;
207
+ const maxTextLength = policy?.maxTextLength;
208
+ if (maxTextLength !== void 0 && [updated.title, updated.description].some((text) => text !== void 0 && text.length > maxTextLength)) {
209
+ return;
210
+ }
211
+ onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
212
+ },
213
+ [onChange, policy, schema]
214
+ );
215
+ const addOption = useCallback(
216
+ (fieldId) => {
217
+ const field = schema.fields.find((candidate) => candidate.id === fieldId);
218
+ if (field === void 0 || !("options" in field)) {
219
+ return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
220
+ }
221
+ if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField) {
222
+ return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
223
+ }
224
+ const existingIds = new Set(
225
+ schema.fields.flatMap(
226
+ (candidate) => "options" in candidate ? candidate.options.map((option2) => option2.id) : []
227
+ )
228
+ );
229
+ const optionId = createId("option", existingIds);
230
+ if (optionId === void 0) {
231
+ return {
232
+ success: false,
233
+ error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
234
+ };
235
+ }
236
+ const option = { id: optionId, label: `Option ${field.options.length + 1}` };
237
+ onChange({
238
+ ...schema,
239
+ fields: schema.fields.map(
240
+ (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: [...candidate.options, option] } : candidate
241
+ )
242
+ });
243
+ return { success: true };
244
+ },
245
+ [createId, onChange, policy, schema]
246
+ );
247
+ const removeOption = useCallback(
248
+ (fieldId, optionId) => {
249
+ const field = schema.fields.find((candidate) => candidate.id === fieldId);
250
+ if (field === void 0 || !("options" in field) || field.options.length <= 1) return;
251
+ onChange({
252
+ ...schema,
253
+ fields: schema.fields.map(
254
+ (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: candidate.options.filter((option) => option.id !== optionId) } : candidate
255
+ )
256
+ });
257
+ },
258
+ [onChange, schema]
259
+ );
260
+ const moveOption = useCallback(
261
+ (fieldId, optionId, targetIndex) => {
262
+ const field = schema.fields.find((candidate) => candidate.id === fieldId);
263
+ if (field === void 0 || !("options" in field)) return;
264
+ const options = move(
265
+ field.options,
266
+ field.options.findIndex((option) => option.id === optionId),
267
+ targetIndex
268
+ );
269
+ if (options === void 0) return;
270
+ onChange({
271
+ ...schema,
272
+ fields: schema.fields.map(
273
+ (candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options } : candidate
274
+ )
275
+ });
276
+ },
277
+ [onChange, schema]
278
+ );
279
+ const addPage = useCallback(
280
+ (questionId) => {
281
+ const existingIds = new Set(schema.pages?.map((page) => page.id) ?? []);
282
+ const pageId = createId("page", existingIds);
283
+ if (pageId === void 0) return;
284
+ if (schema.pages === void 0) {
285
+ onChange({ ...schema, pages: [{ id: pageId, questionIds: schema.fields.map((field) => field.id) }] });
286
+ return;
287
+ }
288
+ const movableQuestionId = questionId ?? schema.pages.find((page) => page.questionIds.length > 1)?.questionIds.at(-1);
289
+ if (movableQuestionId === void 0) return;
290
+ const sourcePage = schema.pages.find((page) => page.questionIds.includes(movableQuestionId));
291
+ if (sourcePage === void 0 || sourcePage.questionIds.length <= 1) return;
292
+ const pages = [
293
+ ...schema.pages.map((page) => ({
294
+ ...page,
295
+ questionIds: page.questionIds.filter((id) => id !== movableQuestionId)
296
+ })),
297
+ { id: pageId, questionIds: [movableQuestionId] }
298
+ ];
299
+ onChange({ ...schema, pages });
300
+ },
301
+ [createId, onChange, schema]
302
+ );
303
+ const removePage = useCallback(
304
+ (pageId) => {
305
+ if (schema.pages === void 0) return;
306
+ const index = schema.pages.findIndex((page) => page.id === pageId);
307
+ const removed = schema.pages[index];
308
+ if (removed === void 0) return;
309
+ if (schema.pages.length === 1) {
310
+ const { pages: _pages, ...singlePage } = schema;
311
+ onChange(singlePage);
312
+ return;
313
+ }
314
+ const targetIndex = index === 0 ? 1 : index - 1;
315
+ onChange({
316
+ ...schema,
317
+ pages: schema.pages.map(
318
+ (page, pageIndex) => pageIndex === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
319
+ ).filter((page) => page.id !== pageId)
320
+ });
321
+ },
322
+ [onChange, schema]
323
+ );
324
+ const setLocaleTranslation = useCallback(
325
+ (locale, target, property, text) => {
326
+ if (locale.trim().length === 0 || policy?.maxTextLength !== void 0 && text.length > policy.maxTextLength)
327
+ return;
328
+ const supportedLocales = [.../* @__PURE__ */ new Set([...schema.supportedLocales ?? [], locale])];
329
+ if (target === "form" && ["title", "description", "completionMessage"].includes(property)) {
330
+ onChange({
331
+ ...schema,
332
+ supportedLocales,
333
+ translations: { ...schema.translations, [locale]: { ...schema.translations?.[locale], [property]: text } }
334
+ });
335
+ return;
336
+ }
337
+ const fields = schema.fields.map((field) => {
338
+ if (field.id === target && ["title", "description"].includes(property)) {
339
+ return {
340
+ ...field,
341
+ translations: { ...field.translations, [locale]: { ...field.translations?.[locale], [property]: text } }
342
+ };
343
+ }
344
+ if (!("options" in field) || property !== "label") return field;
345
+ return {
346
+ ...field,
347
+ options: field.options.map(
348
+ (option) => option.id === target ? { ...option, translations: { ...option.translations, [locale]: text } } : option
349
+ )
350
+ };
351
+ });
352
+ const pages = schema.pages?.map(
353
+ (page) => page.id === target && ["title", "description"].includes(property) ? {
354
+ ...page,
355
+ translations: { ...page.translations, [locale]: { ...page.translations?.[locale], [property]: text } }
356
+ } : page
357
+ );
358
+ onChange({ ...schema, supportedLocales, fields, ...pages === void 0 ? {} : { pages } });
359
+ },
360
+ [onChange, policy, schema]
361
+ );
362
+ const validationIssues = useMemo(() => {
363
+ const result = validateFormSchema(schema);
364
+ const issues = result.valid ? [] : [...result.issues];
365
+ addPolicyIssues(schema, policy, issues);
366
+ return issues;
367
+ }, [policy, schema]);
368
+ return {
369
+ schema,
370
+ addField,
371
+ removeField,
372
+ moveField,
373
+ updateField,
374
+ addOption,
375
+ removeOption,
376
+ moveOption,
377
+ addPage,
378
+ removePage,
379
+ setLocaleTranslation,
380
+ validationIssues
381
+ };
382
+ }
383
+
384
+ // src/builder.tsx
3
385
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
386
  var FIELD_TYPES = [
5
387
  "text",
@@ -37,6 +419,22 @@ var BUILDER_DEFAULTS = {
37
419
  "builder.conditionTrue": "true",
38
420
  "builder.conditionFalse": "false",
39
421
  "builder.addQuestion": "Add question",
422
+ "builder.pages": "Page manager",
423
+ "builder.enablePages": "Enable multi-step pages",
424
+ "builder.addPage": "Add page",
425
+ "builder.newPage": "New page",
426
+ "builder.pageTitle": "Page title",
427
+ "builder.pageDescription": "Page description",
428
+ "builder.pageQuestion": "Question to move to the new page",
429
+ "builder.questionPage": "Page",
430
+ "builder.pageCondition": "Page display condition",
431
+ "builder.localization": "Localization",
432
+ "builder.defaultLocale": "Default locale",
433
+ "builder.supportedLocales": "Supported locales",
434
+ "builder.addLocale": "Add locale",
435
+ "builder.editLocale": "Edit locale",
436
+ "builder.autoTranslate": "Translate all text",
437
+ "builder.translationUnavailable": "Provide an async translation adapter to enable automatic translation.",
40
438
  "builder.fieldType.text": "Text",
41
439
  "builder.fieldType.textarea": "Textarea",
42
440
  "builder.fieldType.number": "Number",
@@ -109,14 +507,21 @@ function withoutDisplayCondition(field) {
109
507
  function sanitizeBuilderSchema(schema) {
110
508
  const sanitized = sanitizeSchema(schema);
111
509
  const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
510
+ const fields = sanitized.fields.map((field, index) => {
511
+ const sourceId = field.displayCondition?.questionId;
512
+ if (sourceId === void 0) return field;
513
+ const sourceIndex = indexById.get(sourceId);
514
+ return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
515
+ });
112
516
  return {
113
517
  ...sanitized,
114
- fields: sanitized.fields.map((field, index) => {
115
- const sourceId = field.displayCondition?.questionId;
116
- if (sourceId === void 0) return field;
117
- const sourceIndex = indexById.get(sourceId);
118
- return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
119
- })
518
+ fields,
519
+ ...sanitized.pages === void 0 ? {} : {
520
+ pages: sanitized.pages.map((page) => ({
521
+ ...page,
522
+ questionIds: fields.filter((field) => page.questionIds.includes(field.id)).map((field) => field.id)
523
+ }))
524
+ }
120
525
  };
121
526
  }
122
527
  function conditionWithValue(questionId, operator, value) {
@@ -160,15 +565,32 @@ function ConditionValueEditor({
160
565
  }
161
566
  );
162
567
  }
163
- function FormBuilder({ schema, onChange, locale = "en", translator }) {
568
+ function FormBuilder({
569
+ schema,
570
+ onChange,
571
+ locale = "en",
572
+ translator,
573
+ translationAdapter,
574
+ policy,
575
+ idFactory
576
+ }) {
577
+ const headless = useFormBuilder({
578
+ schema,
579
+ onChange,
580
+ ...policy === void 0 ? {} : { policy },
581
+ ...idFactory === void 0 ? {} : { idFactory }
582
+ });
583
+ const [newPageQuestionId, setNewPageQuestionId] = useState("");
584
+ const [newLocale, setNewLocale] = useState("");
585
+ const [editingLocale, setEditingLocale] = useState("");
586
+ const [isTranslating, setIsTranslating] = useState(false);
587
+ const [translationError, setTranslationError] = useState(null);
164
588
  const translate = (key, params = {}) => {
165
589
  const translated = translator?.translate(key, locale, params);
166
590
  return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
167
591
  };
168
592
  const emitSchema = (candidate) => onChange(sanitizeBuilderSchema(candidate));
169
- const updateField = (fieldId, update) => {
170
- emitSchema({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? update(field) : field) });
171
- };
593
+ const updateField = headless.updateField;
172
594
  const changeType = (fieldId, type) => {
173
595
  emitSchema({
174
596
  ...schema,
@@ -184,10 +606,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
184
606
  })
185
607
  });
186
608
  };
187
- const removeField = (fieldId) => {
188
- if (schema.fields.length === 1) return;
189
- emitSchema({ ...schema, fields: schema.fields.filter((field) => field.id !== fieldId) });
190
- };
609
+ const removeField = headless.removeField;
191
610
  const moveField = (index, offset) => {
192
611
  const target = index + offset;
193
612
  if (target < 0 || target >= schema.fields.length) return;
@@ -199,14 +618,396 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
199
618
  fields[target] = current;
200
619
  emitSchema({ ...schema, fields });
201
620
  };
202
- const addField = () => {
203
- const id = createUniqueId("q", new Set(schema.fields.map((field) => field.id)));
621
+ const addField = () => headless.addField("text");
622
+ const enablePages = () => {
623
+ if (schema.pages !== void 0) return;
624
+ emitSchema({
625
+ ...schema,
626
+ pages: [
627
+ {
628
+ id: createUniqueId("page", /* @__PURE__ */ new Set()),
629
+ title: translate("builder.newPage"),
630
+ questionIds: schema.fields.map((field) => field.id)
631
+ }
632
+ ]
633
+ });
634
+ };
635
+ const pageForField = (fieldId) => schema.pages?.find((page) => page.questionIds.includes(fieldId));
636
+ const movablePageQuestions = schema.pages?.flatMap((page) => page.questionIds.length > 1 ? page.questionIds : []) ?? [];
637
+ const addPage = () => {
638
+ if (schema.pages === void 0) {
639
+ enablePages();
640
+ return;
641
+ }
642
+ const questionId = movablePageQuestions.includes(newPageQuestionId) ? newPageQuestionId : movablePageQuestions[0];
643
+ if (questionId === void 0) return;
644
+ const pageId = createUniqueId("page", new Set(schema.pages.map((page) => page.id)));
645
+ emitSchema({
646
+ ...schema,
647
+ pages: [
648
+ ...schema.pages.map((page) => ({
649
+ ...page,
650
+ questionIds: page.questionIds.filter((id) => id !== questionId)
651
+ })),
652
+ { id: pageId, title: translate("builder.newPage"), questionIds: [questionId] }
653
+ ]
654
+ });
655
+ setNewPageQuestionId("");
656
+ };
657
+ const removePage = (pageIndex) => {
658
+ if (schema.pages === void 0) return;
659
+ const removed = schema.pages[pageIndex];
660
+ if (removed === void 0) return;
661
+ if (schema.pages.length === 1) {
662
+ const { pages: _pages, ...singlePage } = schema;
663
+ emitSchema(singlePage);
664
+ return;
665
+ }
666
+ const targetIndex = pageIndex === 0 ? 1 : pageIndex - 1;
667
+ emitSchema({
668
+ ...schema,
669
+ pages: schema.pages.map(
670
+ (page, index) => index === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
671
+ ).filter((_page, index) => index !== pageIndex)
672
+ });
673
+ };
674
+ const movePage = (pageIndex, offset) => {
675
+ if (schema.pages === void 0) return;
676
+ const target = pageIndex + offset;
677
+ if (target < 0 || target >= schema.pages.length) return;
678
+ const pages = [...schema.pages];
679
+ const current = pages[pageIndex];
680
+ const other = pages[target];
681
+ if (current === void 0 || other === void 0) return;
682
+ pages[pageIndex] = other;
683
+ pages[target] = current;
684
+ emitSchema({ ...schema, pages });
685
+ };
686
+ const updatePage = (pageId, update) => {
687
+ if (schema.pages === void 0) return;
688
+ emitSchema({ ...schema, pages: schema.pages.map((page) => page.id === pageId ? update(page) : page) });
689
+ };
690
+ const assignFieldToPage = (fieldId, pageId) => {
691
+ if (schema.pages === void 0) return;
692
+ emitSchema({
693
+ ...schema,
694
+ pages: schema.pages.map((page) => ({
695
+ ...page,
696
+ questionIds: page.id === pageId ? schema.fields.filter((field) => page.questionIds.includes(field.id) || field.id === fieldId).map((field) => field.id) : page.questionIds.filter((id) => id !== fieldId)
697
+ })).filter((page) => page.questionIds.length > 0)
698
+ });
699
+ };
700
+ const addLocale = () => {
701
+ const normalized = newLocale.trim();
702
+ if (normalized.length === 0) return;
703
+ const supportedLocales = [
704
+ .../* @__PURE__ */ new Set([
705
+ ...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
706
+ ...schema.supportedLocales ?? [],
707
+ normalized
708
+ ])
709
+ ];
710
+ emitSchema({ ...schema, supportedLocales });
711
+ setEditingLocale(normalized);
712
+ setNewLocale("");
713
+ };
714
+ const translateAll = async () => {
715
+ if (translationAdapter === void 0 || editingLocale.length === 0) return;
716
+ setIsTranslating(true);
717
+ setTranslationError(null);
718
+ try {
719
+ const populated = await populateSchemaTranslations(schema, [editingLocale], translationAdapter, {
720
+ overwrite: "all"
721
+ });
722
+ onChange(populated.schema);
723
+ } catch (cause) {
724
+ setTranslationError(cause instanceof Error ? cause.message : String(cause));
725
+ } finally {
726
+ setIsTranslating(false);
727
+ }
728
+ };
729
+ const updateFormTranslation = (key, value) => {
730
+ if (editingLocale.length === 0) return;
731
+ const current = schema.translations?.[editingLocale];
732
+ const next = key === "title" ? value.length === 0 ? { description: current?.description } : { ...current, title: value } : value.length === 0 ? { title: current?.title } : { ...current, description: value };
204
733
  emitSchema({
205
734
  ...schema,
206
- fields: [...schema.fields, { id, type: "text", title: translate("builder.newQuestionTitle"), required: false }]
735
+ translations: {
736
+ ...schema.translations,
737
+ [editingLocale]: {
738
+ ...next.title === void 0 ? {} : { title: next.title },
739
+ ...next.description === void 0 ? {} : { description: next.description }
740
+ }
741
+ }
207
742
  });
208
743
  };
209
744
  return /* @__PURE__ */ jsxs("section", { className: "form-engine-builder", "aria-label": translate("builder.formBuilder"), children: [
745
+ /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__pages", "aria-labelledby": "builder-pages-heading", children: [
746
+ /* @__PURE__ */ jsx("h2", { id: "builder-pages-heading", children: translate("builder.pages") }),
747
+ schema.pages === void 0 ? /* @__PURE__ */ jsx("button", { type: "button", onClick: enablePages, children: translate("builder.enablePages") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
748
+ schema.pages.map((page, pageIndex) => {
749
+ const priorQuestionIds = new Set(schema.pages?.slice(0, pageIndex).flatMap((item) => item.questionIds));
750
+ const availableSources = schema.fields.filter((field) => priorQuestionIds.has(field.id));
751
+ const source = schema.fields.find((field) => field.id === page.displayCondition?.questionId);
752
+ return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__page", children: [
753
+ /* @__PURE__ */ jsx("legend", { children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }),
754
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__toolbar", children: [
755
+ /* @__PURE__ */ jsx(
756
+ "button",
757
+ {
758
+ type: "button",
759
+ disabled: pageIndex === 0,
760
+ "aria-label": translate("builder.moveUp", { title: page.title ?? page.id }),
761
+ onClick: () => movePage(pageIndex, -1),
762
+ children: "\u2191"
763
+ }
764
+ ),
765
+ /* @__PURE__ */ jsx(
766
+ "button",
767
+ {
768
+ type: "button",
769
+ disabled: pageIndex === (schema.pages?.length ?? 0) - 1,
770
+ "aria-label": translate("builder.moveDown", { title: page.title ?? page.id }),
771
+ onClick: () => movePage(pageIndex, 1),
772
+ children: "\u2193"
773
+ }
774
+ ),
775
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => removePage(pageIndex), children: translate("builder.deleteAction") })
776
+ ] }),
777
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
778
+ /* @__PURE__ */ jsxs("label", { children: [
779
+ translate("builder.pageTitle"),
780
+ /* @__PURE__ */ jsx(
781
+ "input",
782
+ {
783
+ value: page.title ?? "",
784
+ onChange: (event) => {
785
+ const value = event.currentTarget.value;
786
+ updatePage(page.id, (current) => {
787
+ if (value.length > 0) return { ...current, title: value };
788
+ const { title: _title, ...withoutTitle } = current;
789
+ return withoutTitle;
790
+ });
791
+ }
792
+ }
793
+ )
794
+ ] }),
795
+ /* @__PURE__ */ jsxs("label", { children: [
796
+ translate("builder.pageDescription"),
797
+ /* @__PURE__ */ jsx(
798
+ "input",
799
+ {
800
+ value: page.description ?? "",
801
+ onChange: (event) => {
802
+ const value = event.currentTarget.value;
803
+ updatePage(page.id, (current) => {
804
+ if (value.length > 0) return { ...current, description: value };
805
+ const { description: _description, ...withoutDescription } = current;
806
+ return withoutDescription;
807
+ });
808
+ }
809
+ }
810
+ )
811
+ ] })
812
+ ] }),
813
+ editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
814
+ /* @__PURE__ */ jsx("strong", { children: editingLocale }),
815
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
816
+ /* @__PURE__ */ jsxs("label", { children: [
817
+ translate("builder.pageTitle"),
818
+ /* @__PURE__ */ jsx(
819
+ "input",
820
+ {
821
+ value: page.translations?.[editingLocale]?.title ?? "",
822
+ onChange: (event) => {
823
+ const value = event.currentTarget.value;
824
+ updatePage(page.id, (current) => ({
825
+ ...current,
826
+ translations: {
827
+ ...current.translations,
828
+ [editingLocale]: {
829
+ ...value.length === 0 ? {} : { title: value },
830
+ ...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
831
+ }
832
+ }
833
+ }));
834
+ }
835
+ }
836
+ )
837
+ ] }),
838
+ /* @__PURE__ */ jsxs("label", { children: [
839
+ translate("builder.pageDescription"),
840
+ /* @__PURE__ */ jsx(
841
+ "input",
842
+ {
843
+ value: page.translations?.[editingLocale]?.description ?? "",
844
+ onChange: (event) => {
845
+ const value = event.currentTarget.value;
846
+ updatePage(page.id, (current) => ({
847
+ ...current,
848
+ translations: {
849
+ ...current.translations,
850
+ [editingLocale]: {
851
+ ...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
852
+ ...value.length === 0 ? {} : { description: value }
853
+ }
854
+ }
855
+ }));
856
+ }
857
+ }
858
+ )
859
+ ] })
860
+ ] })
861
+ ] }),
862
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__condition", children: [
863
+ /* @__PURE__ */ jsxs("label", { children: [
864
+ translate("builder.pageCondition"),
865
+ /* @__PURE__ */ jsxs(
866
+ "select",
867
+ {
868
+ value: page.displayCondition?.questionId ?? "",
869
+ onChange: (event) => {
870
+ const selected = schema.fields.find((field) => field.id === event.currentTarget.value);
871
+ updatePage(page.id, (current) => {
872
+ if (selected === void 0) {
873
+ const { displayCondition: _condition, ...withoutCondition } = current;
874
+ return withoutCondition;
875
+ }
876
+ return {
877
+ ...current,
878
+ displayCondition: conditionWithValue(
879
+ selected.id,
880
+ conditionOperators(selected)[0] ?? "not_empty",
881
+ defaultConditionValue(selected)
882
+ )
883
+ };
884
+ });
885
+ },
886
+ children: [
887
+ /* @__PURE__ */ jsx("option", { value: "", children: translate("builder.alwaysVisible") }),
888
+ availableSources.map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
889
+ ]
890
+ }
891
+ )
892
+ ] }),
893
+ page.displayCondition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
894
+ /* @__PURE__ */ jsx(
895
+ "select",
896
+ {
897
+ "aria-label": translate("builder.conditionOperator"),
898
+ value: page.displayCondition.operator,
899
+ onChange: (event) => {
900
+ const operator = event.currentTarget.value;
901
+ updatePage(page.id, (current) => ({
902
+ ...current,
903
+ displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
904
+ }));
905
+ },
906
+ children: conditionOperators(source).map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
907
+ }
908
+ ),
909
+ /* @__PURE__ */ jsx(
910
+ ConditionValueEditor,
911
+ {
912
+ source,
913
+ condition: page.displayCondition,
914
+ onChange: (condition) => updatePage(page.id, (current) => ({ ...current, displayCondition: condition })),
915
+ translate
916
+ }
917
+ )
918
+ ] }) : null
919
+ ] })
920
+ ] }, page.id);
921
+ }),
922
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__page-add", children: [
923
+ /* @__PURE__ */ jsxs("label", { children: [
924
+ translate("builder.pageQuestion"),
925
+ /* @__PURE__ */ jsxs(
926
+ "select",
927
+ {
928
+ value: newPageQuestionId,
929
+ disabled: movablePageQuestions.length === 0,
930
+ onChange: (event) => setNewPageQuestionId(event.currentTarget.value),
931
+ children: [
932
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
933
+ schema.fields.filter((field) => movablePageQuestions.includes(field.id)).map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
934
+ ]
935
+ }
936
+ )
937
+ ] }),
938
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: movablePageQuestions.length === 0, onClick: addPage, children: translate("builder.addPage") })
939
+ ] })
940
+ ] })
941
+ ] }),
942
+ /* @__PURE__ */ jsxs("section", { className: "form-engine-builder__localization", "aria-labelledby": "builder-localization-heading", children: [
943
+ /* @__PURE__ */ jsx("h2", { id: "builder-localization-heading", children: translate("builder.localization") }),
944
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
945
+ /* @__PURE__ */ jsxs("label", { children: [
946
+ translate("builder.defaultLocale"),
947
+ /* @__PURE__ */ jsx(
948
+ "input",
949
+ {
950
+ value: schema.defaultLocale ?? "",
951
+ onChange: (event) => {
952
+ const value = event.currentTarget.value.trim();
953
+ emitSchema(
954
+ value.length === 0 ? schema : {
955
+ ...schema,
956
+ defaultLocale: value,
957
+ supportedLocales: [.../* @__PURE__ */ new Set([value, ...schema.supportedLocales ?? []])]
958
+ }
959
+ );
960
+ }
961
+ }
962
+ )
963
+ ] }),
964
+ /* @__PURE__ */ jsxs("label", { children: [
965
+ translate("builder.addLocale"),
966
+ /* @__PURE__ */ jsx("input", { value: newLocale, onChange: (event) => setNewLocale(event.currentTarget.value) })
967
+ ] }),
968
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: addLocale, children: translate("builder.addLocale") }),
969
+ /* @__PURE__ */ jsxs("label", { children: [
970
+ translate("builder.editLocale"),
971
+ /* @__PURE__ */ jsxs("select", { value: editingLocale, onChange: (event) => setEditingLocale(event.currentTarget.value), children: [
972
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
973
+ (schema.supportedLocales ?? []).filter((item) => item !== schema.defaultLocale).map((item) => /* @__PURE__ */ jsx("option", { value: item, children: item }, item))
974
+ ] })
975
+ ] }),
976
+ /* @__PURE__ */ jsx(
977
+ "button",
978
+ {
979
+ type: "button",
980
+ disabled: translationAdapter === void 0 || editingLocale.length === 0 || isTranslating,
981
+ onClick: () => void translateAll(),
982
+ children: translate("builder.autoTranslate")
983
+ }
984
+ )
985
+ ] }),
986
+ translationAdapter === void 0 ? /* @__PURE__ */ jsx("p", { children: translate("builder.translationUnavailable") }) : null,
987
+ translationError === null ? null : /* @__PURE__ */ jsx("p", { className: "form-engine-builder__error", children: translationError }),
988
+ editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
989
+ /* @__PURE__ */ jsxs("label", { children: [
990
+ translate("builder.questionTitle"),
991
+ /* @__PURE__ */ jsx(
992
+ "input",
993
+ {
994
+ value: schema.translations?.[editingLocale]?.title ?? "",
995
+ onChange: (event) => updateFormTranslation("title", event.currentTarget.value)
996
+ }
997
+ )
998
+ ] }),
999
+ /* @__PURE__ */ jsxs("label", { children: [
1000
+ translate("builder.pageDescription"),
1001
+ /* @__PURE__ */ jsx(
1002
+ "input",
1003
+ {
1004
+ value: schema.translations?.[editingLocale]?.description ?? "",
1005
+ onChange: (event) => updateFormTranslation("description", event.currentTarget.value)
1006
+ }
1007
+ )
1008
+ ] })
1009
+ ] })
1010
+ ] }),
210
1011
  /* @__PURE__ */ jsx("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
211
1012
  const condition = field.displayCondition;
212
1013
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
@@ -267,7 +1068,9 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
267
1068
  {
268
1069
  value: field.type,
269
1070
  onChange: (event) => changeType(field.id, event.currentTarget.value),
270
- children: FIELD_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
1071
+ children: FIELD_TYPES.filter(
1072
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
1073
+ ).map((type) => /* @__PURE__ */ jsx("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
271
1074
  }
272
1075
  )
273
1076
  ] }),
@@ -283,6 +1086,98 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
283
1086
  translate("builder.required")
284
1087
  ] })
285
1088
  ] }),
1089
+ schema.pages === void 0 ? null : /* @__PURE__ */ jsxs("label", { children: [
1090
+ translate("builder.questionPage"),
1091
+ /* @__PURE__ */ jsx(
1092
+ "select",
1093
+ {
1094
+ value: pageForField(field.id)?.id ?? "",
1095
+ onChange: (event) => assignFieldToPage(field.id, event.currentTarget.value),
1096
+ children: schema.pages.map((page, pageIndex) => /* @__PURE__ */ jsx("option", { value: page.id, children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }, page.id))
1097
+ }
1098
+ )
1099
+ ] }),
1100
+ editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
1101
+ /* @__PURE__ */ jsx("strong", { children: editingLocale }),
1102
+ /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
1103
+ /* @__PURE__ */ jsxs("label", { children: [
1104
+ translate("builder.questionTitle"),
1105
+ /* @__PURE__ */ jsx(
1106
+ "input",
1107
+ {
1108
+ value: field.translations?.[editingLocale]?.title ?? "",
1109
+ onChange: (event) => {
1110
+ const value = event.currentTarget.value;
1111
+ updateField(field.id, (current) => ({
1112
+ ...current,
1113
+ translations: {
1114
+ ...current.translations,
1115
+ [editingLocale]: {
1116
+ ...value.length === 0 ? {} : { title: value },
1117
+ ...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
1118
+ }
1119
+ }
1120
+ }));
1121
+ }
1122
+ }
1123
+ )
1124
+ ] }),
1125
+ /* @__PURE__ */ jsxs("label", { children: [
1126
+ translate("builder.pageDescription"),
1127
+ /* @__PURE__ */ jsx(
1128
+ "input",
1129
+ {
1130
+ value: field.translations?.[editingLocale]?.description ?? "",
1131
+ onChange: (event) => {
1132
+ const value = event.currentTarget.value;
1133
+ updateField(field.id, (current) => ({
1134
+ ...current,
1135
+ translations: {
1136
+ ...current.translations,
1137
+ [editingLocale]: {
1138
+ ...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
1139
+ ...value.length === 0 ? {} : { description: value }
1140
+ }
1141
+ }
1142
+ }));
1143
+ }
1144
+ }
1145
+ )
1146
+ ] })
1147
+ ] }),
1148
+ "options" in field ? field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("label", { children: [
1149
+ translate("builder.optionLabel", { index: optionIndex + 1 }),
1150
+ " (",
1151
+ editingLocale,
1152
+ ")",
1153
+ /* @__PURE__ */ jsx(
1154
+ "input",
1155
+ {
1156
+ value: option.translations?.[editingLocale] ?? "",
1157
+ onChange: (event) => {
1158
+ const value = event.currentTarget.value;
1159
+ updateField(field.id, (current) => {
1160
+ if (!("options" in current)) return current;
1161
+ return {
1162
+ ...current,
1163
+ options: current.options.map(
1164
+ (candidate) => candidate.id === option.id ? {
1165
+ ...candidate,
1166
+ translations: Object.fromEntries([
1167
+ ...Object.entries(candidate.translations ?? {}).filter(
1168
+ ([localeKey]) => localeKey !== editingLocale
1169
+ ),
1170
+ ...value.length === 0 ? [] : [[editingLocale, value]]
1171
+ ])
1172
+ } : candidate
1173
+ )
1174
+ };
1175
+ });
1176
+ }
1177
+ }
1178
+ )
1179
+ ] }, option.id)) : null
1180
+ ] }),
286
1181
  field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
287
1182
  /* @__PURE__ */ jsxs("label", { children: [
288
1183
  translate("builder.minimum"),
@@ -348,13 +1243,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
348
1243
  {
349
1244
  type: "button",
350
1245
  disabled: field.options.length === 1,
351
- onClick: () => updateField(
352
- field.id,
353
- (current) => "options" in current ? {
354
- ...current,
355
- options: current.options.filter((_item, itemIndex) => itemIndex !== optionIndex)
356
- } : current
357
- ),
1246
+ onClick: () => headless.removeOption(field.id, option.id),
358
1247
  children: translate("builder.remove")
359
1248
  }
360
1249
  )
@@ -363,22 +1252,8 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
363
1252
  "button",
364
1253
  {
365
1254
  type: "button",
366
- onClick: () => updateField(
367
- field.id,
368
- (current) => "options" in current ? (() => {
369
- const id = createUniqueId("opt", new Set(current.options.map((option) => option.id)));
370
- return {
371
- ...current,
372
- options: [
373
- ...current.options,
374
- {
375
- id,
376
- label: translate("builder.newOptionLabel", { index: current.options.length + 1 })
377
- }
378
- ]
379
- };
380
- })() : current
381
- ),
1255
+ disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
1256
+ onClick: () => headless.addOption(field.id),
382
1257
  children: translate("builder.addOption")
383
1258
  }
384
1259
  )
@@ -440,7 +1315,16 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
440
1315
  ] })
441
1316
  ] }, field.id);
442
1317
  }) }),
443
- /* @__PURE__ */ jsx("button", { className: "form-engine-builder__add", type: "button", onClick: addField, children: translate("builder.addQuestion") })
1318
+ /* @__PURE__ */ jsx(
1319
+ "button",
1320
+ {
1321
+ className: "form-engine-builder__add",
1322
+ type: "button",
1323
+ disabled: policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields,
1324
+ onClick: addField,
1325
+ children: translate("builder.addQuestion")
1326
+ }
1327
+ )
444
1328
  ] });
445
1329
  }
446
1330
 
@@ -448,10 +1332,13 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
448
1332
  import {
449
1333
  assertValidFormSchema,
450
1334
  calculateFieldVisibility,
1335
+ calculatePageVisibility,
1336
+ resolveLocalizedSchema,
451
1337
  selectVisibleAnswers,
452
- validateAnswers
1338
+ validateAnswers,
1339
+ validatePageAnswers
453
1340
  } from "@form-engine-ts/core";
454
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
1341
+ import { createContext, useCallback as useCallback2, useContext, useEffect, useMemo as useMemo2, useState as useState2 } from "react";
455
1342
  import { jsx as jsx2 } from "react/jsx-runtime";
456
1343
  var FormContext = createContext(null);
457
1344
  function issuesByField(issues) {
@@ -468,26 +1355,30 @@ function FormProvider({
468
1355
  onSubmit,
469
1356
  children
470
1357
  }) {
471
- const validSchema = useMemo(() => {
1358
+ const validSchema = useMemo2(() => {
472
1359
  assertValidFormSchema(schema);
473
- return schema;
474
- }, [schema]);
475
- const [values, setValues] = useState(() => ({ ...initialValues }));
476
- const [errors, setErrors] = useState({});
477
- const [submitStatus, setSubmitStatus] = useState("idle");
478
- const [submitError, setSubmitError] = useState(null);
479
- const visibility = useMemo(() => calculateFieldVisibility(validSchema, values), [validSchema, values]);
1360
+ const localized = resolveLocalizedSchema(schema, locale);
1361
+ assertValidFormSchema(localized);
1362
+ return localized;
1363
+ }, [locale, schema]);
1364
+ const [values, setValues] = useState2(() => ({ ...initialValues }));
1365
+ const [errors, setErrors] = useState2({});
1366
+ const [submitStatus, setSubmitStatus] = useState2("idle");
1367
+ const [submitError, setSubmitError] = useState2(null);
1368
+ const [validationPageIndex, setValidationPageIndex] = useState2(null);
1369
+ const visibility = useMemo2(() => calculateFieldVisibility(validSchema, values), [validSchema, values]);
1370
+ const pageVisibility = useMemo2(() => calculatePageVisibility(validSchema, values), [validSchema, values]);
480
1371
  useEffect(() => {
481
1372
  const fieldIds = new Set(validSchema.fields.map((field) => field.id));
482
1373
  setValues((current) => Object.fromEntries(Object.entries(current).filter(([fieldId]) => fieldIds.has(fieldId))));
483
1374
  }, [validSchema]);
484
- const setValue = useCallback(
1375
+ const setValue = useCallback2(
485
1376
  (fieldId, value) => {
486
1377
  setValues((current) => {
487
1378
  const next = { ...current, [fieldId]: value };
488
1379
  setErrors((currentErrors) => {
489
1380
  if (Object.keys(currentErrors).length === 0) return currentErrors;
490
- const result = validateAnswers(validSchema, next);
1381
+ const result = validationPageIndex === null ? validateAnswers(validSchema, next) : validatePageAnswers(validSchema, validationPageIndex, next);
491
1382
  return issuesByField(result.issues);
492
1383
  });
493
1384
  return next;
@@ -495,52 +1386,89 @@ function FormProvider({
495
1386
  setSubmitStatus((current) => current === "success" || current === "error" ? "idle" : current);
496
1387
  setSubmitError(null);
497
1388
  },
1389
+ [validSchema, validationPageIndex]
1390
+ );
1391
+ const restoreValues = useCallback2(
1392
+ (restoredValues) => {
1393
+ const fieldIds = new Set(validSchema.fields.map((field) => field.id));
1394
+ setValues(Object.fromEntries(Object.entries(restoredValues).filter(([fieldId]) => fieldIds.has(fieldId))));
1395
+ setErrors({});
1396
+ setValidationPageIndex(null);
1397
+ setSubmitStatus("idle");
1398
+ setSubmitError(null);
1399
+ },
498
1400
  [validSchema]
499
1401
  );
500
- const reset = useCallback(() => {
1402
+ const validatePage = useCallback2(
1403
+ (pageIndex) => {
1404
+ const result = validatePageAnswers(validSchema, pageIndex, values);
1405
+ setErrors(issuesByField(result.issues));
1406
+ setValidationPageIndex(result.valid ? null : pageIndex);
1407
+ setSubmitStatus("idle");
1408
+ setSubmitError(null);
1409
+ return result;
1410
+ },
1411
+ [validSchema, values]
1412
+ );
1413
+ const reset = useCallback2(() => {
501
1414
  setValues({ ...initialValues });
502
1415
  setErrors({});
1416
+ setValidationPageIndex(null);
503
1417
  setSubmitStatus("idle");
504
1418
  setSubmitError(null);
505
1419
  }, [initialValues]);
506
- const submit = useCallback(async () => {
507
- const validation = validateAnswers(validSchema, values);
508
- if (!validation.valid) {
509
- setErrors(issuesByField(validation.issues));
510
- setSubmitStatus("error");
1420
+ const submit = useCallback2(
1421
+ async (beforeSubmit) => {
1422
+ const validation = validateAnswers(validSchema, values);
1423
+ if (!validation.valid) {
1424
+ setErrors(issuesByField(validation.issues));
1425
+ setValidationPageIndex(null);
1426
+ setSubmitStatus("error");
1427
+ setSubmitError(null);
1428
+ return { status: "invalid", issues: validation.issues };
1429
+ }
1430
+ setErrors({});
1431
+ setValidationPageIndex(null);
511
1432
  setSubmitError(null);
512
- return false;
513
- }
514
- setErrors({});
515
- setSubmitStatus("submitting");
516
- setSubmitError(null);
517
- try {
518
- await onSubmit(selectVisibleAnswers(validSchema, values));
519
- if (resetOnSuccess) setValues({ ...initialValues });
520
- setSubmitStatus("success");
521
- return true;
522
- } catch (cause) {
523
- setSubmitError(cause instanceof Error ? cause : new Error(String(cause)));
524
- setSubmitStatus("error");
525
- return false;
526
- }
527
- }, [initialValues, onSubmit, resetOnSuccess, validSchema, values]);
528
- const translate = useCallback(
1433
+ const visibleValues = selectVisibleAnswers(validSchema, values);
1434
+ try {
1435
+ if (beforeSubmit !== void 0 && await beforeSubmit(visibleValues) === "cancel") {
1436
+ setSubmitStatus("idle");
1437
+ return { status: "cancelled" };
1438
+ }
1439
+ setSubmitStatus("submitting");
1440
+ await onSubmit(visibleValues);
1441
+ if (resetOnSuccess) setValues({ ...initialValues });
1442
+ setSubmitStatus("success");
1443
+ return { status: "success" };
1444
+ } catch (cause) {
1445
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1446
+ setSubmitError(error);
1447
+ setSubmitStatus("error");
1448
+ return { status: "error", error };
1449
+ }
1450
+ },
1451
+ [initialValues, onSubmit, resetOnSuccess, validSchema, values]
1452
+ );
1453
+ const translate = useCallback2(
529
1454
  (key, params) => translator.translate(key, locale, params),
530
1455
  [locale, translator]
531
1456
  );
532
- const contextValue = useMemo(
1457
+ const contextValue = useMemo2(
533
1458
  () => ({
534
1459
  schema: validSchema,
535
1460
  locale,
536
1461
  translator,
537
1462
  values,
538
1463
  visibility,
1464
+ pageVisibility,
539
1465
  errors,
540
1466
  submitStatus,
541
1467
  submitError,
542
1468
  isSubmitting: submitStatus === "submitting",
543
1469
  setValue,
1470
+ restoreValues,
1471
+ validatePage,
544
1472
  reset,
545
1473
  submit,
546
1474
  translate
@@ -548,13 +1476,16 @@ function FormProvider({
548
1476
  [
549
1477
  errors,
550
1478
  locale,
1479
+ pageVisibility,
551
1480
  reset,
1481
+ restoreValues,
552
1482
  setValue,
553
1483
  submit,
554
1484
  submitError,
555
1485
  submitStatus,
556
1486
  translate,
557
1487
  translator,
1488
+ validatePage,
558
1489
  validSchema,
559
1490
  values,
560
1491
  visibility
@@ -571,7 +1502,7 @@ function useField(fieldId) {
571
1502
  const form = useForm();
572
1503
  const field = form.schema.fields.find((item) => item.id === fieldId);
573
1504
  if (field === void 0) throw new Error(`Unknown form field: ${fieldId}`);
574
- const setValue = useCallback((value) => form.setValue(fieldId, value), [fieldId, form]);
1505
+ const setValue = useCallback2((value) => form.setValue(fieldId, value), [fieldId, form]);
575
1506
  return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
576
1507
  }
577
1508
 
@@ -579,8 +1510,15 @@ function useField(fieldId) {
579
1510
  import {
580
1511
  validateAnswers as validateAnswers2
581
1512
  } from "@form-engine-ts/core";
582
- import { useId } from "react";
583
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1513
+ import {
1514
+ Fragment as Fragment2,
1515
+ useEffect as useEffect2,
1516
+ useId,
1517
+ useMemo as useMemo3,
1518
+ useRef,
1519
+ useState as useState3
1520
+ } from "react";
1521
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
584
1522
  function describedBy(field, error, helpId, errorId) {
585
1523
  const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
586
1524
  Boolean
@@ -594,7 +1532,7 @@ function RequiredMark({ required }) {
594
1532
  ] }) : null;
595
1533
  }
596
1534
  function FieldMessage({ props }) {
597
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1535
+ return /* @__PURE__ */ jsxs2(Fragment3, { children: [
598
1536
  props.field.description === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.helpId, className: "fe-help", children: props.field.description }),
599
1537
  props.error === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
600
1538
  ] });
@@ -758,59 +1696,287 @@ function DefaultField(props) {
758
1696
  /* @__PURE__ */ jsx3(FieldMessage, { props })
759
1697
  ] });
760
1698
  }
761
- function FormRenderer({
1699
+ function isRecord(value) {
1700
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1701
+ }
1702
+ function isFormValue(value) {
1703
+ return value === void 0 || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every((item) => typeof item === "string");
1704
+ }
1705
+ function parseDraft(serialized) {
1706
+ try {
1707
+ const value = JSON.parse(serialized);
1708
+ if (!isRecord(value) || typeof value.formId !== "string" || typeof value.formVersion !== "number" || !Number.isInteger(value.formVersion) || typeof value.savedAt !== "string" || !isRecord(value.values) || !Object.values(value.values).every(isFormValue)) {
1709
+ return null;
1710
+ }
1711
+ return {
1712
+ formId: value.formId,
1713
+ formVersion: value.formVersion,
1714
+ savedAt: value.savedAt,
1715
+ values: Object.fromEntries(
1716
+ Object.entries(value.values).filter((entry) => isFormValue(entry[1]))
1717
+ )
1718
+ };
1719
+ } catch {
1720
+ return null;
1721
+ }
1722
+ }
1723
+ function ContextFormRenderer({
762
1724
  components = {},
763
1725
  className = "",
764
1726
  successMessageKey,
765
- errorMessageKey
1727
+ errorMessageKey,
1728
+ autoSaveKey,
1729
+ beforeSubmit,
1730
+ onDraftSave,
1731
+ slots = {}
766
1732
  }) {
767
1733
  const form = useForm();
768
1734
  const prefix = useId().replace(/:/g, "");
769
- const handleSubmit = async (event) => {
770
- event.preventDefault();
771
- const formElement = event.currentTarget;
1735
+ const formRef = useRef(null);
1736
+ const loadedDraftKey = useRef(null);
1737
+ const [draftRestored, setDraftRestored] = useState3(false);
1738
+ const [currentPageIndex, setCurrentPageIndex] = useState3(0);
1739
+ const [focusFieldId, setFocusFieldId] = useState3(null);
1740
+ const pages = form.schema.pages;
1741
+ const visiblePageIndexes = useMemo3(
1742
+ () => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
1743
+ [form.pageVisibility, pages]
1744
+ );
1745
+ const activePage = pages?.[currentPageIndex];
1746
+ const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
1747
+ const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
1748
+ useEffect2(() => {
1749
+ if (pages === void 0 || visiblePageIndexes.length === 0) {
1750
+ setCurrentPageIndex(0);
1751
+ return;
1752
+ }
1753
+ if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
1754
+ }, [currentPageIndex, pages, visiblePageIndexes]);
1755
+ useEffect2(() => {
1756
+ if (focusFieldId === null) return;
1757
+ const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
1758
+ (element) => element.dataset.fieldId === focusFieldId
1759
+ );
1760
+ const control = fieldContainer?.querySelector("input, select, textarea");
1761
+ if (control !== void 0 && control !== null) {
1762
+ control.focus();
1763
+ setFocusFieldId(null);
1764
+ }
1765
+ }, [focusFieldId]);
1766
+ useEffect2(() => {
1767
+ if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
1768
+ const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
1769
+ if (loadedDraftKey.current === loadIdentity) return;
1770
+ loadedDraftKey.current = loadIdentity;
1771
+ const serialized = globalThis.localStorage.getItem(autoSaveKey);
1772
+ if (serialized === null) return;
1773
+ const draft = parseDraft(serialized);
1774
+ if (draft === null || draft.formId !== form.schema.id || draft.formVersion !== form.schema.version) return;
1775
+ form.restoreValues(draft.values);
1776
+ setDraftRestored(true);
1777
+ }, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
1778
+ useEffect2(() => {
1779
+ if (form.submitStatus === "success") return;
1780
+ const timeout = globalThis.setTimeout(() => {
1781
+ onDraftSave?.(form.values);
1782
+ if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
1783
+ const draft = {
1784
+ formId: form.schema.id,
1785
+ formVersion: form.schema.version,
1786
+ values: form.values,
1787
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
1788
+ };
1789
+ globalThis.localStorage.setItem(autoSaveKey, JSON.stringify(draft));
1790
+ }, 500);
1791
+ return () => globalThis.clearTimeout(timeout);
1792
+ }, [autoSaveKey, form.schema.id, form.schema.version, form.submitStatus, form.values, onDraftSave]);
1793
+ const focusFirstIssue = (fieldId) => {
1794
+ if (fieldId !== void 0) setFocusFieldId(fieldId);
1795
+ };
1796
+ const handleNext = () => {
1797
+ const result = form.validatePage(currentPageIndex);
1798
+ if (!result.valid) {
1799
+ focusFirstIssue(result.issues[0]?.fieldId);
1800
+ return;
1801
+ }
1802
+ const nextPageIndex = visiblePageIndexes[activeVisibleIndex + 1];
1803
+ if (nextPageIndex !== void 0) setCurrentPageIndex(nextPageIndex);
1804
+ };
1805
+ const submitValues = async () => {
772
1806
  const validation = validateAnswers2(form.schema, form.values);
773
1807
  const firstInvalidFieldId = validation.issues[0]?.fieldId;
774
- const valid = await form.submit();
775
- if (!valid && firstInvalidFieldId !== void 0) {
776
- queueMicrotask(() => {
777
- const fieldContainer = [...formElement.querySelectorAll("[data-field-id]")].find(
778
- (element) => element.dataset.fieldId === firstInvalidFieldId
779
- );
780
- fieldContainer?.querySelector("input, select, textarea")?.focus();
781
- });
1808
+ const result = await form.submit(beforeSubmit);
1809
+ if (result.status === "invalid") {
1810
+ const invalidPageIndex = pages?.findIndex(
1811
+ (page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
1812
+ );
1813
+ if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
1814
+ focusFirstIssue(firstInvalidFieldId);
1815
+ return result;
1816
+ }
1817
+ if (result.status !== "success") return result;
1818
+ if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
1819
+ globalThis.localStorage.removeItem(autoSaveKey);
1820
+ setDraftRestored(false);
782
1821
  }
1822
+ setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
1823
+ return result;
1824
+ };
1825
+ const handleSubmit = (event) => {
1826
+ event.preventDefault();
1827
+ void submitValues();
783
1828
  };
784
- return /* @__PURE__ */ jsxs2("form", { className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
785
- /* @__PURE__ */ jsxs2("header", { className: "fe-header", children: [
1829
+ const validationIssues = Object.values(form.errors).filter((issue) => issue !== void 0);
1830
+ const canPrev = pages !== void 0 && activeVisibleIndex > 0;
1831
+ const canNext = pages !== void 0 && activeVisibleIndex < visiblePageIndexes.length - 1;
1832
+ const renderSubmitButton = () => slots.renderSubmitButton?.({ isSubmitting: form.isSubmitting, onSubmit: () => void submitValues() }) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
1833
+ return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
1834
+ slots.renderHeader?.({
1835
+ title: form.schema.title,
1836
+ ...form.schema.description === void 0 ? {} : { description: form.schema.description }
1837
+ }) ?? /* @__PURE__ */ jsxs2("header", { className: "fe-header", children: [
786
1838
  /* @__PURE__ */ jsx3("h1", { children: form.schema.title }),
787
- form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description })
1839
+ form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description }),
1840
+ pages === void 0 ? null : /* @__PURE__ */ jsxs2("div", { className: "fe-progress", children: [
1841
+ /* @__PURE__ */ jsx3(
1842
+ "div",
1843
+ {
1844
+ className: "form-progress-bar",
1845
+ role: "progressbar",
1846
+ "aria-valuemin": 1,
1847
+ "aria-valuemax": visiblePageIndexes.length,
1848
+ "aria-valuenow": activeVisibleIndex + 1,
1849
+ children: /* @__PURE__ */ jsx3(
1850
+ "div",
1851
+ {
1852
+ className: "form-progress-fill",
1853
+ style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
1854
+ }
1855
+ )
1856
+ }
1857
+ ),
1858
+ /* @__PURE__ */ jsx3("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
1859
+ ] }),
1860
+ draftRestored ? /* @__PURE__ */ jsx3("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
788
1861
  ] }),
789
- /* @__PURE__ */ jsx3("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true).map((field) => {
1862
+ activePage?.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
1863
+ activePage?.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description }),
1864
+ /* @__PURE__ */ jsx3("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
1865
+ const error = form.errors[field.id];
790
1866
  const props = {
791
1867
  field,
792
1868
  value: form.values[field.id],
793
- error: form.errors[field.id],
1869
+ error,
794
1870
  setValue: (value) => form.setValue(field.id, value),
795
1871
  translate: form.translate,
796
1872
  inputId: `${prefix}-${field.id}`,
797
1873
  errorId: `${prefix}-${field.id}-error`,
798
1874
  helpId: `${prefix}-${field.id}-help`
799
1875
  };
1876
+ if (slots.renderField !== void 0) {
1877
+ return /* @__PURE__ */ jsx3(Fragment2, { children: slots.renderField({
1878
+ question: field,
1879
+ value: form.values[field.id],
1880
+ onChange: (value) => {
1881
+ if (isFormValue(value)) form.setValue(field.id, value);
1882
+ },
1883
+ ...error === void 0 ? {} : { error }
1884
+ }) }, field.id);
1885
+ }
800
1886
  const Component = components[field.type];
801
1887
  return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
802
1888
  }) }),
803
- /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") }),
1889
+ validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
1890
+ validationIssues.length,
1891
+ " validation error",
1892
+ validationIssues.length === 1 ? "" : "s",
1893
+ "."
1894
+ ] }),
1895
+ pages === void 0 ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
1896
+ slots.renderNavigation?.({
1897
+ currentPage: 0,
1898
+ totalPages: 1,
1899
+ canPrev: false,
1900
+ canNext: false,
1901
+ onPrev: () => void 0,
1902
+ onNext: () => void 0
1903
+ }),
1904
+ renderSubmitButton()
1905
+ ] }) : /* @__PURE__ */ jsxs2("div", { className: "form-step-navigation", children: [
1906
+ slots.renderNavigation?.({
1907
+ currentPage: activeVisibleIndex,
1908
+ totalPages: visiblePageIndexes.length,
1909
+ canPrev,
1910
+ canNext,
1911
+ onPrev: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
1912
+ onNext: handleNext
1913
+ }) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
1914
+ canPrev ? /* @__PURE__ */ jsx3(
1915
+ "button",
1916
+ {
1917
+ className: "btn-prev",
1918
+ type: "button",
1919
+ onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
1920
+ children: form.translate("form.back")
1921
+ }
1922
+ ) : null,
1923
+ canNext ? /* @__PURE__ */ jsx3("button", { className: "btn-next", type: "button", onClick: handleNext, children: form.translate("form.next") }) : null
1924
+ ] }),
1925
+ canNext ? null : renderSubmitButton()
1926
+ ] }),
804
1927
  /* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
805
- form.submitStatus === "success" && successMessageKey !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "status", children: form.translate(successMessageKey) }) : null,
1928
+ form.submitStatus === "success" ? slots.renderCompletion?.({
1929
+ message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
1930
+ }) ?? /* @__PURE__ */ jsx3("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
806
1931
  form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
807
1932
  ] })
808
1933
  ] });
809
1934
  }
1935
+ var RENDERER_MESSAGES = {
1936
+ "form.submit": "Submit",
1937
+ "form.back": "Back",
1938
+ "form.next": "Next",
1939
+ "form.step": "Step {{current}} / {{total}}",
1940
+ "form.draftRestored": "Draft restored",
1941
+ "validation.required": "This field is required."
1942
+ };
1943
+ var defaultRendererTranslator = {
1944
+ translate(key, _locale, params = {}) {
1945
+ return (RENDERER_MESSAGES[key] ?? key).replace(
1946
+ /\{\{(\w+)\}\}/g,
1947
+ (token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
1948
+ );
1949
+ }
1950
+ };
1951
+ function FormRenderer(props) {
1952
+ if (!("schema" in props)) return /* @__PURE__ */ jsx3(ContextFormRenderer, { ...props });
1953
+ const {
1954
+ schema,
1955
+ locale = schema.defaultLocale ?? "en",
1956
+ translator = defaultRendererTranslator,
1957
+ initialValues,
1958
+ resetOnSuccess,
1959
+ onSubmit,
1960
+ ...rendererProps
1961
+ } = props;
1962
+ return /* @__PURE__ */ jsx3(
1963
+ FormProvider,
1964
+ {
1965
+ schema,
1966
+ locale,
1967
+ translator,
1968
+ onSubmit,
1969
+ ...initialValues === void 0 ? {} : { initialValues },
1970
+ ...resetOnSuccess === void 0 ? {} : { resetOnSuccess },
1971
+ children: /* @__PURE__ */ jsx3(ContextFormRenderer, { ...rendererProps })
1972
+ }
1973
+ );
1974
+ }
810
1975
  export {
811
1976
  FormBuilder,
812
1977
  FormProvider,
813
1978
  FormRenderer,
814
1979
  useField,
815
- useForm
1980
+ useForm,
1981
+ useFormBuilder
816
1982
  };