@fogpipe/forma-react 0.17.1 → 0.18.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.
Files changed (36) hide show
  1. package/README.md +82 -0
  2. package/dist/FormRenderer-D_ZVK44t.d.ts +558 -0
  3. package/dist/chunk-5K4QITFH.js +1276 -0
  4. package/dist/chunk-5K4QITFH.js.map +1 -0
  5. package/dist/defaults/index.d.ts +56 -0
  6. package/dist/defaults/index.js +895 -0
  7. package/dist/defaults/index.js.map +1 -0
  8. package/dist/defaults/styles/forma-defaults.css +696 -0
  9. package/dist/index.d.ts +7 -559
  10. package/dist/index.js +28 -1292
  11. package/dist/index.js.map +1 -1
  12. package/package.json +17 -3
  13. package/src/__tests__/defaults/components.test.tsx +818 -0
  14. package/src/__tests__/defaults/integration.test.tsx +494 -0
  15. package/src/__tests__/defaults/layout.test.tsx +298 -0
  16. package/src/defaults/DefaultFormRenderer.tsx +43 -0
  17. package/src/defaults/componentMap.ts +45 -0
  18. package/src/defaults/components/ArrayField.tsx +183 -0
  19. package/src/defaults/components/BooleanInput.tsx +32 -0
  20. package/src/defaults/components/ComputedDisplay.tsx +26 -0
  21. package/src/defaults/components/DateInput.tsx +59 -0
  22. package/src/defaults/components/DisplayField.tsx +15 -0
  23. package/src/defaults/components/FallbackField.tsx +35 -0
  24. package/src/defaults/components/MatrixField.tsx +98 -0
  25. package/src/defaults/components/MultiSelectInput.tsx +51 -0
  26. package/src/defaults/components/NumberInput.tsx +73 -0
  27. package/src/defaults/components/ObjectField.tsx +22 -0
  28. package/src/defaults/components/SelectInput.tsx +44 -0
  29. package/src/defaults/components/TextInput.tsx +48 -0
  30. package/src/defaults/components/TextareaInput.tsx +46 -0
  31. package/src/defaults/index.ts +33 -0
  32. package/src/defaults/layout/FieldWrapper.tsx +83 -0
  33. package/src/defaults/layout/FormLayout.tsx +34 -0
  34. package/src/defaults/layout/PageWrapper.tsx +18 -0
  35. package/src/defaults/layout/WizardLayout.tsx +130 -0
  36. package/src/defaults/styles/forma-defaults.css +696 -0
@@ -0,0 +1,1276 @@
1
+ // src/useForma.ts
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+ import { isAdornableField } from "@fogpipe/forma-core";
11
+
12
+ // src/events.ts
13
+ var FormaEventEmitter = class {
14
+ listeners = /* @__PURE__ */ new Map();
15
+ /**
16
+ * Register a listener for an event. Returns an unsubscribe function.
17
+ */
18
+ on(event, listener) {
19
+ if (!this.listeners.has(event)) {
20
+ this.listeners.set(event, /* @__PURE__ */ new Set());
21
+ }
22
+ this.listeners.get(event).add(listener);
23
+ return () => {
24
+ const set = this.listeners.get(event);
25
+ if (set) {
26
+ set.delete(listener);
27
+ if (set.size === 0) {
28
+ this.listeners.delete(event);
29
+ }
30
+ }
31
+ };
32
+ }
33
+ /**
34
+ * Fire an event synchronously. Listener errors are caught and logged
35
+ * to prevent one listener from breaking others.
36
+ */
37
+ fire(event, payload) {
38
+ const set = this.listeners.get(event);
39
+ if (!set || set.size === 0) return;
40
+ for (const listener of set) {
41
+ try {
42
+ listener(payload);
43
+ } catch (error) {
44
+ console.error(`[forma] Error in "${event}" event listener:`, error);
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Fire an event and await all async listeners sequentially.
50
+ * Used for preSubmit where handlers can be async.
51
+ */
52
+ async fireAsync(event, payload) {
53
+ const set = this.listeners.get(event);
54
+ if (!set || set.size === 0) return;
55
+ for (const listener of set) {
56
+ try {
57
+ await listener(payload);
58
+ } catch (error) {
59
+ console.error(`[forma] Error in "${event}" event listener:`, error);
60
+ }
61
+ }
62
+ }
63
+ /**
64
+ * Check if any listeners are registered for an event.
65
+ */
66
+ hasListeners(event) {
67
+ const set = this.listeners.get(event);
68
+ return set !== void 0 && set.size > 0;
69
+ }
70
+ /**
71
+ * Remove all listeners. Called on cleanup.
72
+ */
73
+ clear() {
74
+ this.listeners.clear();
75
+ }
76
+ };
77
+
78
+ // src/useForma.ts
79
+ import {
80
+ getVisibility,
81
+ getRequired,
82
+ getEnabled,
83
+ getReadonly,
84
+ validate,
85
+ calculate,
86
+ getPageVisibility,
87
+ getOptionsVisibility
88
+ } from "@fogpipe/forma-core";
89
+ function formReducer(state, action) {
90
+ switch (action.type) {
91
+ case "SET_FIELD_VALUE":
92
+ return {
93
+ ...state,
94
+ data: { ...state.data, [action.field]: action.value },
95
+ isDirty: true,
96
+ isSubmitted: false
97
+ // Clear on data change
98
+ };
99
+ case "SET_FIELD_TOUCHED":
100
+ return {
101
+ ...state,
102
+ touched: { ...state.touched, [action.field]: action.touched }
103
+ };
104
+ case "SET_VALUES":
105
+ return {
106
+ ...state,
107
+ data: { ...state.data, ...action.values },
108
+ isDirty: true,
109
+ isSubmitted: false
110
+ // Clear on data change
111
+ };
112
+ case "SET_SUBMITTING":
113
+ return { ...state, isSubmitting: action.isSubmitting };
114
+ case "SET_SUBMITTED":
115
+ return { ...state, isSubmitted: action.isSubmitted };
116
+ case "SET_PAGE":
117
+ return { ...state, currentPage: action.page };
118
+ case "RESET":
119
+ return {
120
+ data: action.initialData,
121
+ touched: {},
122
+ isSubmitting: false,
123
+ isSubmitted: false,
124
+ isDirty: false,
125
+ currentPage: 0
126
+ };
127
+ default:
128
+ return state;
129
+ }
130
+ }
131
+ function getDefaultBooleanValues(spec) {
132
+ var _a;
133
+ const defaults = {};
134
+ for (const fieldPath of spec.fieldOrder) {
135
+ const schemaProperty = (_a = spec.schema.properties) == null ? void 0 : _a[fieldPath];
136
+ const fieldDef = spec.fields[fieldPath];
137
+ if ((schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean") {
138
+ defaults[fieldPath] = false;
139
+ }
140
+ }
141
+ return defaults;
142
+ }
143
+ function getFieldDefaults(spec) {
144
+ const defaults = {};
145
+ for (const [fieldPath, fieldDef] of Object.entries(spec.fields)) {
146
+ if (fieldDef.defaultValue !== void 0) {
147
+ defaults[fieldPath] = fieldDef.defaultValue;
148
+ }
149
+ }
150
+ return defaults;
151
+ }
152
+ function useForma(options) {
153
+ const {
154
+ spec: inputSpec,
155
+ initialData = {},
156
+ onSubmit,
157
+ onChange,
158
+ validateOn = "blur",
159
+ referenceData,
160
+ validationDebounceMs = 0,
161
+ on: onEvents
162
+ } = options;
163
+ const spec = useMemo(() => {
164
+ if (!referenceData) return inputSpec;
165
+ return {
166
+ ...inputSpec,
167
+ referenceData: {
168
+ ...inputSpec.referenceData,
169
+ ...referenceData
170
+ }
171
+ };
172
+ }, [inputSpec, referenceData]);
173
+ const [state, dispatch] = useReducer(formReducer, {
174
+ data: {
175
+ ...getDefaultBooleanValues(spec),
176
+ ...getFieldDefaults(spec),
177
+ ...initialData
178
+ },
179
+ touched: {},
180
+ isSubmitting: false,
181
+ isSubmitted: false,
182
+ isDirty: false,
183
+ currentPage: 0
184
+ });
185
+ const stateDataRef = useRef(state.data);
186
+ stateDataRef.current = state.data;
187
+ const hasInitialized = useRef(false);
188
+ const emitterRef = useRef(new FormaEventEmitter());
189
+ const onEventsRef = useRef(onEvents);
190
+ onEventsRef.current = onEvents;
191
+ const pendingEventsRef = useRef([]);
192
+ const isFiringEventsRef = useRef(false);
193
+ useEffect(() => {
194
+ const emitter = emitterRef.current;
195
+ return () => {
196
+ emitter.clear();
197
+ };
198
+ }, []);
199
+ const fireEvent = useCallback(
200
+ (event, payload) => {
201
+ var _a;
202
+ try {
203
+ const handler = (_a = onEventsRef.current) == null ? void 0 : _a[event];
204
+ if (handler) handler(payload);
205
+ } catch (error) {
206
+ console.error(`[forma] Error in "${event}" event handler:`, error);
207
+ }
208
+ emitterRef.current.fire(event, payload);
209
+ },
210
+ []
211
+ );
212
+ const computed = useMemo(
213
+ () => calculate(state.data, spec),
214
+ [state.data, spec]
215
+ );
216
+ const visibility = useMemo(
217
+ () => getVisibility(state.data, spec, { computed }),
218
+ [state.data, spec, computed]
219
+ );
220
+ const required = useMemo(
221
+ () => getRequired(state.data, spec, { computed }),
222
+ [state.data, spec, computed]
223
+ );
224
+ const enabled = useMemo(
225
+ () => getEnabled(state.data, spec, { computed }),
226
+ [state.data, spec, computed]
227
+ );
228
+ const readonly = useMemo(
229
+ () => getReadonly(state.data, spec, { computed }),
230
+ [state.data, spec, computed]
231
+ );
232
+ const optionsVisibility = useMemo(
233
+ () => getOptionsVisibility(state.data, spec, { computed }),
234
+ [state.data, spec, computed]
235
+ );
236
+ const immediateValidation = useMemo(
237
+ () => validate(state.data, spec, { computed, onlyVisible: true }),
238
+ [state.data, spec, computed]
239
+ );
240
+ const [debouncedValidation, setDebouncedValidation] = useState(immediateValidation);
241
+ useEffect(() => {
242
+ if (validationDebounceMs <= 0) {
243
+ setDebouncedValidation(immediateValidation);
244
+ return;
245
+ }
246
+ const timeoutId = setTimeout(() => {
247
+ setDebouncedValidation(immediateValidation);
248
+ }, validationDebounceMs);
249
+ return () => clearTimeout(timeoutId);
250
+ }, [immediateValidation, validationDebounceMs]);
251
+ const validation = validationDebounceMs > 0 ? debouncedValidation : immediateValidation;
252
+ useEffect(() => {
253
+ if (hasInitialized.current) {
254
+ onChange == null ? void 0 : onChange(state.data, computed);
255
+ } else {
256
+ hasInitialized.current = true;
257
+ }
258
+ }, [state.data, computed, onChange]);
259
+ const setNestedValue = useCallback(
260
+ (path, value) => {
261
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
262
+ if (parts.length === 1) {
263
+ dispatch({ type: "SET_FIELD_VALUE", field: path, value });
264
+ return;
265
+ }
266
+ const buildNestedObject = (data, pathParts, val) => {
267
+ const result = { ...data };
268
+ let current = result;
269
+ for (let i = 0; i < pathParts.length - 1; i++) {
270
+ const part = pathParts[i];
271
+ const nextPart = pathParts[i + 1];
272
+ const isNextArrayIndex = /^\d+$/.test(nextPart);
273
+ if (current[part] === void 0) {
274
+ current[part] = isNextArrayIndex ? [] : {};
275
+ } else if (Array.isArray(current[part])) {
276
+ current[part] = [...current[part]];
277
+ } else {
278
+ current[part] = { ...current[part] };
279
+ }
280
+ current = current[part];
281
+ }
282
+ current[pathParts[pathParts.length - 1]] = val;
283
+ return result;
284
+ };
285
+ dispatch({
286
+ type: "SET_VALUES",
287
+ values: buildNestedObject(state.data, parts, value)
288
+ });
289
+ },
290
+ [state.data]
291
+ );
292
+ const getValueAtPath = useCallback((path) => {
293
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
294
+ let value = stateDataRef.current;
295
+ for (const part of parts) {
296
+ if (value === null || value === void 0) return void 0;
297
+ value = value[part];
298
+ }
299
+ return value;
300
+ }, []);
301
+ const queueFieldChangedEvent = useCallback(
302
+ (path, value, source) => {
303
+ if (isFiringEventsRef.current) return;
304
+ const previousValue = getValueAtPath(path);
305
+ if (previousValue === value) return;
306
+ pendingEventsRef.current.push({
307
+ event: "fieldChanged",
308
+ payload: { path, value, previousValue, source }
309
+ });
310
+ },
311
+ [getValueAtPath]
312
+ );
313
+ const setFieldValue = useCallback(
314
+ (path, value) => {
315
+ queueFieldChangedEvent(path, value, "user");
316
+ setNestedValue(path, value);
317
+ if (validateOn === "change") {
318
+ dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched: true });
319
+ }
320
+ },
321
+ [validateOn, setNestedValue, queueFieldChangedEvent]
322
+ );
323
+ const setFieldTouched = useCallback((path, touched = true) => {
324
+ dispatch({ type: "SET_FIELD_TOUCHED", field: path, touched });
325
+ }, []);
326
+ const setValues = useCallback(
327
+ (values) => {
328
+ for (const [key, value] of Object.entries(values)) {
329
+ queueFieldChangedEvent(key, value, "setValues");
330
+ }
331
+ dispatch({ type: "SET_VALUES", values });
332
+ },
333
+ [queueFieldChangedEvent]
334
+ );
335
+ const validateField = useCallback(
336
+ (path) => {
337
+ return validation.errors.filter((e) => e.field === path);
338
+ },
339
+ [validation]
340
+ );
341
+ const validateForm = useCallback(() => {
342
+ return validation;
343
+ }, [validation]);
344
+ const submitForm = useCallback(async () => {
345
+ var _a;
346
+ dispatch({ type: "SET_SUBMITTING", isSubmitting: true });
347
+ const submissionData = { ...state.data };
348
+ let postSubmitPayload;
349
+ try {
350
+ const preSubmitPayload = {
351
+ data: submissionData,
352
+ computed: { ...computed }
353
+ };
354
+ const declarativePreSubmit = (_a = onEventsRef.current) == null ? void 0 : _a.preSubmit;
355
+ if (declarativePreSubmit) {
356
+ await declarativePreSubmit(preSubmitPayload);
357
+ }
358
+ if (emitterRef.current.hasListeners("preSubmit")) {
359
+ await emitterRef.current.fireAsync("preSubmit", preSubmitPayload);
360
+ }
361
+ if (!immediateValidation.valid) {
362
+ postSubmitPayload = {
363
+ data: submissionData,
364
+ success: false,
365
+ validationErrors: immediateValidation.errors
366
+ };
367
+ } else if (onSubmit) {
368
+ try {
369
+ await onSubmit(submissionData);
370
+ postSubmitPayload = { data: submissionData, success: true };
371
+ } catch (error) {
372
+ postSubmitPayload = {
373
+ data: submissionData,
374
+ success: false,
375
+ error: error instanceof Error ? error : new Error(String(error))
376
+ };
377
+ }
378
+ } else {
379
+ postSubmitPayload = { data: submissionData, success: true };
380
+ }
381
+ dispatch({ type: "SET_SUBMITTED", isSubmitted: true });
382
+ } finally {
383
+ dispatch({ type: "SET_SUBMITTING", isSubmitting: false });
384
+ if (postSubmitPayload) {
385
+ fireEvent("postSubmit", postSubmitPayload);
386
+ }
387
+ }
388
+ }, [immediateValidation, onSubmit, state.data, computed, fireEvent]);
389
+ const resetForm = useCallback(() => {
390
+ const resetData = {
391
+ ...getDefaultBooleanValues(spec),
392
+ ...getFieldDefaults(spec),
393
+ ...initialData
394
+ };
395
+ if (!isFiringEventsRef.current) {
396
+ const currentData = stateDataRef.current;
397
+ const allKeys = /* @__PURE__ */ new Set([
398
+ ...Object.keys(currentData),
399
+ ...Object.keys(resetData)
400
+ ]);
401
+ for (const key of allKeys) {
402
+ const currentVal = currentData[key];
403
+ const resetVal = resetData[key];
404
+ if (currentVal !== resetVal) {
405
+ pendingEventsRef.current.push({
406
+ event: "fieldChanged",
407
+ payload: {
408
+ path: key,
409
+ value: resetVal,
410
+ previousValue: currentVal,
411
+ source: "reset"
412
+ }
413
+ });
414
+ }
415
+ }
416
+ pendingEventsRef.current.push({
417
+ event: "formReset",
418
+ payload: {}
419
+ });
420
+ }
421
+ dispatch({ type: "RESET", initialData: resetData });
422
+ }, [spec, initialData]);
423
+ const wizard = useMemo(() => {
424
+ if (!spec.pages || spec.pages.length === 0) return null;
425
+ const pageVisibility = getPageVisibility(state.data, spec, { computed });
426
+ const pages = spec.pages.map((p) => ({
427
+ id: p.id,
428
+ title: p.title,
429
+ description: p.description,
430
+ visible: pageVisibility[p.id] !== false,
431
+ fields: p.fields
432
+ }));
433
+ const visiblePages = pages.filter((p) => p.visible);
434
+ const maxPageIndex = Math.max(0, visiblePages.length - 1);
435
+ const clampedPageIndex = Math.min(
436
+ Math.max(0, state.currentPage),
437
+ maxPageIndex
438
+ );
439
+ if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {
440
+ dispatch({ type: "SET_PAGE", page: clampedPageIndex });
441
+ }
442
+ const currentPage = visiblePages[clampedPageIndex] || null;
443
+ const hasNextPage = clampedPageIndex < visiblePages.length - 1;
444
+ const hasPreviousPage = clampedPageIndex > 0;
445
+ const isLastPage = clampedPageIndex === visiblePages.length - 1;
446
+ const advanceToNextPage = () => {
447
+ if (hasNextPage) {
448
+ const toIndex = clampedPageIndex + 1;
449
+ dispatch({ type: "SET_PAGE", page: toIndex });
450
+ const newPage = visiblePages[toIndex];
451
+ if (newPage) {
452
+ fireEvent("pageChanged", {
453
+ fromIndex: clampedPageIndex,
454
+ toIndex,
455
+ page: newPage
456
+ });
457
+ }
458
+ }
459
+ };
460
+ return {
461
+ pages,
462
+ currentPageIndex: clampedPageIndex,
463
+ currentPage,
464
+ goToPage: (index) => {
465
+ const validIndex = Math.min(Math.max(0, index), maxPageIndex);
466
+ if (validIndex !== clampedPageIndex) {
467
+ dispatch({ type: "SET_PAGE", page: validIndex });
468
+ const newPage = visiblePages[validIndex];
469
+ if (newPage) {
470
+ fireEvent("pageChanged", {
471
+ fromIndex: clampedPageIndex,
472
+ toIndex: validIndex,
473
+ page: newPage
474
+ });
475
+ }
476
+ }
477
+ },
478
+ nextPage: advanceToNextPage,
479
+ previousPage: () => {
480
+ if (hasPreviousPage) {
481
+ const toIndex = clampedPageIndex - 1;
482
+ dispatch({ type: "SET_PAGE", page: toIndex });
483
+ const newPage = visiblePages[toIndex];
484
+ if (newPage) {
485
+ fireEvent("pageChanged", {
486
+ fromIndex: clampedPageIndex,
487
+ toIndex,
488
+ page: newPage
489
+ });
490
+ }
491
+ }
492
+ },
493
+ // Same function as nextPage — exposed as a separate name so consumers can
494
+ // bind a single "Next" button without risk of accidentally triggering submission.
495
+ // nextPage is already a no-op on the last page.
496
+ handleNext: advanceToNextPage,
497
+ hasNextPage,
498
+ hasPreviousPage,
499
+ canProceed: (() => {
500
+ if (!currentPage) return true;
501
+ const pageErrors = validation.errors.filter((e) => {
502
+ const isOnCurrentPage = currentPage.fields.includes(e.field) || currentPage.fields.some((f) => e.field.startsWith(`${f}[`));
503
+ const isVisible = visibility[e.field] !== false;
504
+ const isError = e.severity === "error";
505
+ return isOnCurrentPage && isVisible && isError;
506
+ });
507
+ return pageErrors.length === 0;
508
+ })(),
509
+ isLastPage,
510
+ touchCurrentPageFields: () => {
511
+ if (currentPage) {
512
+ currentPage.fields.forEach((field) => {
513
+ dispatch({ type: "SET_FIELD_TOUCHED", field, touched: true });
514
+ });
515
+ }
516
+ },
517
+ validateCurrentPage: () => {
518
+ if (!currentPage) return true;
519
+ const pageErrors = validation.errors.filter(
520
+ (e) => currentPage.fields.includes(e.field)
521
+ );
522
+ return pageErrors.length === 0;
523
+ }
524
+ };
525
+ }, [
526
+ spec,
527
+ state.data,
528
+ state.currentPage,
529
+ computed,
530
+ validation,
531
+ visibility,
532
+ fireEvent
533
+ ]);
534
+ useEffect(() => {
535
+ const events = pendingEventsRef.current;
536
+ if (events.length === 0) return;
537
+ pendingEventsRef.current = [];
538
+ isFiringEventsRef.current = true;
539
+ try {
540
+ for (const pending of events) {
541
+ fireEvent(
542
+ pending.event,
543
+ pending.payload
544
+ );
545
+ }
546
+ } finally {
547
+ isFiringEventsRef.current = false;
548
+ }
549
+ });
550
+ const setValueAtPath = useCallback(
551
+ (path, value) => {
552
+ queueFieldChangedEvent(path, value, "user");
553
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
554
+ if (parts.length === 1) {
555
+ dispatch({ type: "SET_FIELD_VALUE", field: path, value });
556
+ return;
557
+ }
558
+ const newData = { ...stateDataRef.current };
559
+ let current = newData;
560
+ for (let i = 0; i < parts.length - 1; i++) {
561
+ const part = parts[i];
562
+ const nextPart = parts[i + 1];
563
+ const isNextArrayIndex = /^\d+$/.test(nextPart);
564
+ if (current[part] === void 0) {
565
+ current[part] = isNextArrayIndex ? [] : {};
566
+ } else if (Array.isArray(current[part])) {
567
+ current[part] = [...current[part]];
568
+ } else {
569
+ current[part] = { ...current[part] };
570
+ }
571
+ current = current[part];
572
+ }
573
+ current[parts[parts.length - 1]] = value;
574
+ dispatch({ type: "SET_VALUES", values: newData });
575
+ },
576
+ [queueFieldChangedEvent]
577
+ );
578
+ const fieldHandlers = useRef(/* @__PURE__ */ new Map());
579
+ useEffect(() => {
580
+ const validFields = new Set(spec.fieldOrder);
581
+ for (const fieldId of spec.fieldOrder) {
582
+ const fieldDef = spec.fields[fieldId];
583
+ if ((fieldDef == null ? void 0 : fieldDef.type) === "array" && fieldDef.itemFields) {
584
+ for (const key of fieldHandlers.current.keys()) {
585
+ if (key.startsWith(`${fieldId}[`)) {
586
+ validFields.add(key);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ for (const key of fieldHandlers.current.keys()) {
592
+ const baseField = key.split("[")[0];
593
+ if (!validFields.has(key) && !validFields.has(baseField)) {
594
+ fieldHandlers.current.delete(key);
595
+ }
596
+ }
597
+ }, [spec]);
598
+ const getFieldHandlers = useCallback(
599
+ (path) => {
600
+ if (!fieldHandlers.current.has(path)) {
601
+ fieldHandlers.current.set(path, {
602
+ onChange: (value) => setValueAtPath(path, value),
603
+ onBlur: () => setFieldTouched(path)
604
+ });
605
+ }
606
+ return fieldHandlers.current.get(path);
607
+ },
608
+ [setValueAtPath, setFieldTouched]
609
+ );
610
+ const getFieldProps = useCallback(
611
+ (path) => {
612
+ var _a;
613
+ const fieldDef = spec.fields[path];
614
+ const handlers = getFieldHandlers(path);
615
+ let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
616
+ if (!fieldType || fieldType === "computed") {
617
+ const schemaProperty2 = spec.schema.properties[path];
618
+ if (schemaProperty2) {
619
+ if (schemaProperty2.type === "number") fieldType = "number";
620
+ else if (schemaProperty2.type === "integer") fieldType = "integer";
621
+ else if (schemaProperty2.type === "boolean") fieldType = "boolean";
622
+ else if (schemaProperty2.type === "array") fieldType = "array";
623
+ else if (schemaProperty2.type === "object") fieldType = "object";
624
+ else if ("enum" in schemaProperty2 && schemaProperty2.enum)
625
+ fieldType = "select";
626
+ else if ("format" in schemaProperty2) {
627
+ if (schemaProperty2.format === "date") fieldType = "date";
628
+ else if (schemaProperty2.format === "date-time")
629
+ fieldType = "datetime";
630
+ else if (schemaProperty2.format === "email") fieldType = "email";
631
+ else if (schemaProperty2.format === "uri") fieldType = "url";
632
+ }
633
+ }
634
+ }
635
+ const fieldErrors = validation.errors.filter((e) => e.field === path);
636
+ const isTouched = state.touched[path] ?? false;
637
+ const shouldShowErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
638
+ const visibleFieldErrors = shouldShowErrors ? fieldErrors : [];
639
+ const hasVisibleErrors = visibleFieldErrors.length > 0;
640
+ const isRequired = required[path] ?? false;
641
+ const schemaProperty = spec.schema.properties[path];
642
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
643
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
644
+ const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
645
+ const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
646
+ return {
647
+ name: path,
648
+ value: getValueAtPath(path),
649
+ type: fieldType,
650
+ label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
651
+ description: fieldDef == null ? void 0 : fieldDef.description,
652
+ placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
653
+ visible: visibility[path] !== false,
654
+ enabled: enabled[path] !== false,
655
+ readonly: readonly[path] ?? false,
656
+ required: isRequired,
657
+ showRequiredIndicator,
658
+ touched: isTouched,
659
+ errors: fieldErrors,
660
+ visibleErrors: visibleFieldErrors,
661
+ onChange: handlers.onChange,
662
+ onBlur: handlers.onBlur,
663
+ // ARIA accessibility attributes (driven by visibleErrors, not all errors)
664
+ "aria-invalid": hasVisibleErrors || void 0,
665
+ "aria-describedby": hasVisibleErrors ? `${path}-error` : void 0,
666
+ "aria-required": isRequired || void 0,
667
+ // Adorner props (only for adornable field types)
668
+ ...adornerProps,
669
+ // Presentation variant
670
+ variant: fieldDef == null ? void 0 : fieldDef.variant,
671
+ variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
672
+ };
673
+ },
674
+ [
675
+ spec,
676
+ state.touched,
677
+ state.isSubmitted,
678
+ visibility,
679
+ enabled,
680
+ readonly,
681
+ required,
682
+ validation.errors,
683
+ validateOn,
684
+ getValueAtPath,
685
+ getFieldHandlers
686
+ ]
687
+ );
688
+ const getSelectFieldProps = useCallback(
689
+ (path) => {
690
+ const baseProps = getFieldProps(path);
691
+ const visibleOptions = optionsVisibility[path] ?? [];
692
+ return {
693
+ ...baseProps,
694
+ options: visibleOptions
695
+ };
696
+ },
697
+ [getFieldProps, optionsVisibility]
698
+ );
699
+ const getArrayHelpers = useCallback(
700
+ (path) => {
701
+ const fieldDef = spec.fields[path];
702
+ const currentValue = getValueAtPath(path) ?? [];
703
+ const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
704
+ const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
705
+ const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
706
+ const canAdd = currentValue.length < maxItems;
707
+ const canRemove = currentValue.length > minItems;
708
+ const getItemFieldProps = (index, fieldName) => {
709
+ var _a;
710
+ const itemPath = `${path}[${index}].${fieldName}`;
711
+ const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
712
+ const handlers = getFieldHandlers(itemPath);
713
+ const item = currentValue[index] ?? {};
714
+ const itemValue = item[fieldName];
715
+ const fieldErrors = validation.errors.filter(
716
+ (e) => e.field === itemPath
717
+ );
718
+ const isTouched = state.touched[itemPath] ?? false;
719
+ const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
720
+ const visibleOptions = optionsVisibility[itemPath];
721
+ return {
722
+ name: itemPath,
723
+ value: itemValue,
724
+ type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
725
+ label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
726
+ description: itemFieldDef == null ? void 0 : itemFieldDef.description,
727
+ placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
728
+ visible: true,
729
+ enabled: enabled[path] !== false,
730
+ readonly: readonly[itemPath] ?? false,
731
+ required: false,
732
+ // TODO: Evaluate item field required
733
+ showRequiredIndicator: false,
734
+ // Item fields don't show required indicator
735
+ touched: isTouched,
736
+ errors: fieldErrors,
737
+ visibleErrors: showErrors ? fieldErrors : [],
738
+ onChange: handlers.onChange,
739
+ onBlur: handlers.onBlur,
740
+ options: visibleOptions
741
+ };
742
+ };
743
+ return {
744
+ items: currentValue,
745
+ push: (item) => {
746
+ if (canAdd) {
747
+ setValueAtPath(path, [...currentValue, item]);
748
+ }
749
+ },
750
+ remove: (index) => {
751
+ if (canRemove) {
752
+ const newArray = [...currentValue];
753
+ newArray.splice(index, 1);
754
+ setValueAtPath(path, newArray);
755
+ }
756
+ },
757
+ move: (from, to) => {
758
+ const newArray = [...currentValue];
759
+ const [item] = newArray.splice(from, 1);
760
+ newArray.splice(to, 0, item);
761
+ setValueAtPath(path, newArray);
762
+ },
763
+ swap: (indexA, indexB) => {
764
+ const newArray = [...currentValue];
765
+ [newArray[indexA], newArray[indexB]] = [
766
+ newArray[indexB],
767
+ newArray[indexA]
768
+ ];
769
+ setValueAtPath(path, newArray);
770
+ },
771
+ insert: (index, item) => {
772
+ if (canAdd) {
773
+ const newArray = [...currentValue];
774
+ newArray.splice(index, 0, item);
775
+ setValueAtPath(path, newArray);
776
+ }
777
+ },
778
+ getItemFieldProps,
779
+ minItems,
780
+ maxItems,
781
+ canAdd,
782
+ canRemove
783
+ };
784
+ },
785
+ [
786
+ spec.fields,
787
+ getValueAtPath,
788
+ setValueAtPath,
789
+ getFieldHandlers,
790
+ enabled,
791
+ readonly,
792
+ state.touched,
793
+ state.isSubmitted,
794
+ validation.errors,
795
+ validateOn,
796
+ optionsVisibility
797
+ ]
798
+ );
799
+ const on = useCallback(
800
+ (event, listener) => emitterRef.current.on(event, listener),
801
+ []
802
+ );
803
+ return useMemo(
804
+ () => ({
805
+ data: state.data,
806
+ computed,
807
+ visibility,
808
+ required,
809
+ enabled,
810
+ readonly,
811
+ optionsVisibility,
812
+ touched: state.touched,
813
+ errors: validation.errors,
814
+ isValid: validation.valid,
815
+ isSubmitting: state.isSubmitting,
816
+ isSubmitted: state.isSubmitted,
817
+ isDirty: state.isDirty,
818
+ spec,
819
+ wizard,
820
+ setFieldValue,
821
+ setFieldTouched,
822
+ setValues,
823
+ validateField,
824
+ validateForm,
825
+ submitForm,
826
+ resetForm,
827
+ on,
828
+ getFieldProps,
829
+ getSelectFieldProps,
830
+ getArrayHelpers
831
+ }),
832
+ [
833
+ state.data,
834
+ state.touched,
835
+ state.isSubmitting,
836
+ state.isSubmitted,
837
+ state.isDirty,
838
+ computed,
839
+ visibility,
840
+ required,
841
+ enabled,
842
+ readonly,
843
+ optionsVisibility,
844
+ validation.errors,
845
+ validation.valid,
846
+ spec,
847
+ wizard,
848
+ setFieldValue,
849
+ setFieldTouched,
850
+ setValues,
851
+ validateField,
852
+ validateForm,
853
+ submitForm,
854
+ resetForm,
855
+ on,
856
+ getFieldProps,
857
+ getSelectFieldProps,
858
+ getArrayHelpers
859
+ ]
860
+ );
861
+ }
862
+
863
+ // src/context.ts
864
+ import { createContext, useContext } from "react";
865
+ var FormaContext = createContext(null);
866
+ function useFormaContext() {
867
+ const context = useContext(FormaContext);
868
+ if (!context) {
869
+ throw new Error(
870
+ "useFormaContext must be used within a FormaContext.Provider"
871
+ );
872
+ }
873
+ return context;
874
+ }
875
+
876
+ // src/FormRenderer.tsx
877
+ import React, {
878
+ forwardRef,
879
+ useImperativeHandle,
880
+ useRef as useRef2,
881
+ useMemo as useMemo2,
882
+ useCallback as useCallback2
883
+ } from "react";
884
+ import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
885
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
886
+ function DefaultLayout({ children, onSubmit, isSubmitting }) {
887
+ return /* @__PURE__ */ jsxs("form", { onSubmit, children: [
888
+ children,
889
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: isSubmitting, children: isSubmitting ? "Submitting..." : "Submit" })
890
+ ] });
891
+ }
892
+ function DefaultFieldWrapper({
893
+ fieldPath,
894
+ field,
895
+ children,
896
+ errors,
897
+ showRequiredIndicator,
898
+ visible
899
+ }) {
900
+ if (!visible) return null;
901
+ const errorId = `${fieldPath}-error`;
902
+ const descriptionId = field.description ? `${fieldPath}-description` : void 0;
903
+ const hasErrors = errors.length > 0;
904
+ return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
905
+ field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
906
+ field.label,
907
+ showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
908
+ showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
909
+ ] }),
910
+ children,
911
+ hasErrors && /* @__PURE__ */ jsx(
912
+ "div",
913
+ {
914
+ id: errorId,
915
+ className: "field-errors",
916
+ role: "alert",
917
+ "aria-live": "polite",
918
+ children: errors.map((error, i) => /* @__PURE__ */ jsx("span", { className: "error", children: error.message }, i))
919
+ }
920
+ ),
921
+ field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
922
+ ] });
923
+ }
924
+ function DefaultPageWrapper({
925
+ title,
926
+ description,
927
+ children
928
+ }) {
929
+ return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
930
+ /* @__PURE__ */ jsx("h2", { children: title }),
931
+ description && /* @__PURE__ */ jsx("p", { children: description }),
932
+ children
933
+ ] });
934
+ }
935
+ function getNumberConstraints(schema) {
936
+ if (!schema) return {};
937
+ if (schema.type !== "number" && schema.type !== "integer") return {};
938
+ const min = "minimum" in schema && typeof schema.minimum === "number" ? schema.minimum : void 0;
939
+ const max = "maximum" in schema && typeof schema.maximum === "number" ? schema.maximum : void 0;
940
+ let step;
941
+ if ("multipleOf" in schema && typeof schema.multipleOf === "number") {
942
+ step = schema.multipleOf;
943
+ } else if (schema.type === "integer") {
944
+ step = 1;
945
+ }
946
+ return { min, max, step };
947
+ }
948
+ function createDefaultItem(itemFields) {
949
+ const item = {};
950
+ for (const [fieldName, fieldDef] of Object.entries(itemFields)) {
951
+ if (fieldDef.defaultValue !== void 0) {
952
+ item[fieldName] = fieldDef.defaultValue;
953
+ } else if (fieldDef.type === "boolean") {
954
+ item[fieldName] = false;
955
+ } else if (fieldDef.type === "number" || fieldDef.type === "integer") {
956
+ item[fieldName] = null;
957
+ } else {
958
+ item[fieldName] = "";
959
+ }
960
+ }
961
+ return item;
962
+ }
963
+ var FormRenderer = forwardRef(
964
+ function FormRenderer2(props, ref) {
965
+ const {
966
+ spec,
967
+ initialData,
968
+ onSubmit,
969
+ onChange,
970
+ components,
971
+ layout: Layout = DefaultLayout,
972
+ fieldWrapper: FieldWrapper = DefaultFieldWrapper,
973
+ pageWrapper: PageWrapper = DefaultPageWrapper,
974
+ validateOn = "blur"
975
+ } = props;
976
+ const forma = useForma({
977
+ spec,
978
+ initialData,
979
+ onSubmit,
980
+ onChange,
981
+ validateOn
982
+ });
983
+ const fieldRefs = useRef2(/* @__PURE__ */ new Map());
984
+ const focusField = useCallback2((path) => {
985
+ const element = fieldRefs.current.get(path);
986
+ element == null ? void 0 : element.focus();
987
+ }, []);
988
+ const focusFirstError = useCallback2(() => {
989
+ const firstError = forma.errors[0];
990
+ if (firstError) {
991
+ focusField(firstError.field);
992
+ }
993
+ }, [forma.errors, focusField]);
994
+ useImperativeHandle(
995
+ ref,
996
+ () => ({
997
+ submitForm: forma.submitForm,
998
+ resetForm: forma.resetForm,
999
+ validateForm: forma.validateForm,
1000
+ focusField,
1001
+ focusFirstError,
1002
+ getValues: () => forma.data,
1003
+ setValues: forma.setValues,
1004
+ isValid: forma.isValid,
1005
+ isDirty: forma.isDirty
1006
+ }),
1007
+ [forma, focusField, focusFirstError]
1008
+ );
1009
+ const {
1010
+ data: formaData,
1011
+ computed: formaComputed,
1012
+ visibility: formaVisibility,
1013
+ required: formaRequired,
1014
+ enabled: formaEnabled,
1015
+ readonly: formaReadonly,
1016
+ optionsVisibility: formaOptionsVisibility,
1017
+ touched: formaTouched,
1018
+ errors: formaErrors,
1019
+ isSubmitted: formaIsSubmitted,
1020
+ setFieldValue,
1021
+ setFieldTouched,
1022
+ getArrayHelpers
1023
+ } = forma;
1024
+ const fieldsToRender = useMemo2(() => {
1025
+ var _a;
1026
+ if (spec.pages && spec.pages.length > 0 && forma.wizard) {
1027
+ const currentPage = forma.wizard.currentPage;
1028
+ if (currentPage) {
1029
+ return currentPage.fields;
1030
+ }
1031
+ return ((_a = spec.pages[0]) == null ? void 0 : _a.fields) ?? [];
1032
+ }
1033
+ return spec.fieldOrder;
1034
+ }, [spec.pages, spec.fieldOrder, forma.wizard]);
1035
+ const renderField = useCallback2(
1036
+ (fieldPath) => {
1037
+ var _a;
1038
+ const fieldDef = spec.fields[fieldPath];
1039
+ if (!fieldDef) return null;
1040
+ const isVisible = formaVisibility[fieldPath] !== false;
1041
+ if (!isVisible) {
1042
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true }, fieldPath);
1043
+ }
1044
+ const fieldType = fieldDef.type;
1045
+ const componentKey = fieldType;
1046
+ const Component = components[componentKey] || components.fallback;
1047
+ if (!Component) {
1048
+ console.warn(`No component found for field type: ${fieldType}`);
1049
+ return null;
1050
+ }
1051
+ const errors = formaErrors.filter((e) => e.field === fieldPath);
1052
+ const touched = formaTouched[fieldPath] ?? false;
1053
+ const showErrors = validateOn === "change" || validateOn === "blur" && touched || formaIsSubmitted;
1054
+ const visibleErrors = showErrors ? errors : [];
1055
+ const required = formaRequired[fieldPath] ?? false;
1056
+ const disabled = formaEnabled[fieldPath] === false;
1057
+ const schemaProperty = spec.schema.properties[fieldPath];
1058
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
1059
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
1060
+ const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
1061
+ const isReadonly = formaReadonly[fieldPath] ?? false;
1062
+ const baseProps = {
1063
+ name: fieldPath,
1064
+ field: fieldDef,
1065
+ value: formaData[fieldPath],
1066
+ touched,
1067
+ required,
1068
+ disabled,
1069
+ errors,
1070
+ visibleErrors,
1071
+ onChange: (value) => setFieldValue(fieldPath, value),
1072
+ onBlur: () => setFieldTouched(fieldPath),
1073
+ // Convenience properties
1074
+ visible: true,
1075
+ // Always true since we already filtered for visibility
1076
+ enabled: !disabled,
1077
+ readonly: isReadonly,
1078
+ label: fieldDef.label ?? fieldPath,
1079
+ description: fieldDef.description,
1080
+ placeholder: fieldDef.placeholder,
1081
+ // Adorner properties (only for adornable field types)
1082
+ ...isAdornableField2(fieldDef) && {
1083
+ prefix: fieldDef.prefix,
1084
+ suffix: fieldDef.suffix
1085
+ },
1086
+ // Presentation variant
1087
+ variant: fieldDef.variant,
1088
+ variantConfig: fieldDef.variantConfig
1089
+ };
1090
+ let fieldProps = baseProps;
1091
+ if (fieldType === "number" || fieldType === "integer") {
1092
+ const constraints = getNumberConstraints(schemaProperty);
1093
+ fieldProps = {
1094
+ ...baseProps,
1095
+ fieldType,
1096
+ value: baseProps.value,
1097
+ onChange: baseProps.onChange,
1098
+ ...constraints
1099
+ };
1100
+ } else if (fieldType === "select" || fieldType === "multiselect") {
1101
+ const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
1102
+ fieldProps = {
1103
+ ...baseProps,
1104
+ fieldType,
1105
+ value: baseProps.value,
1106
+ onChange: baseProps.onChange,
1107
+ options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? []
1108
+ };
1109
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
1110
+ const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
1111
+ const minItems = fieldDef.minItems ?? 0;
1112
+ const maxItems = fieldDef.maxItems ?? Infinity;
1113
+ const itemFieldDefs = fieldDef.itemFields;
1114
+ const baseHelpers = getArrayHelpers(fieldPath);
1115
+ const pushWithDefault = (item) => {
1116
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
1117
+ baseHelpers.push(newItem);
1118
+ };
1119
+ const getItemFieldPropsExtended = (index, fieldName) => {
1120
+ const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
1121
+ const itemFieldDef = itemFieldDefs[fieldName];
1122
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
1123
+ return {
1124
+ ...baseProps2,
1125
+ itemIndex: index,
1126
+ fieldName,
1127
+ options: formaOptionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
1128
+ };
1129
+ };
1130
+ const helpers = {
1131
+ items: arrayValue,
1132
+ push: pushWithDefault,
1133
+ insert: baseHelpers.insert,
1134
+ remove: baseHelpers.remove,
1135
+ move: baseHelpers.move,
1136
+ swap: baseHelpers.swap,
1137
+ getItemFieldProps: getItemFieldPropsExtended,
1138
+ minItems,
1139
+ maxItems,
1140
+ canAdd: arrayValue.length < maxItems,
1141
+ canRemove: arrayValue.length > minItems
1142
+ };
1143
+ fieldProps = {
1144
+ ...baseProps,
1145
+ fieldType: "array",
1146
+ value: arrayValue,
1147
+ onChange: baseProps.onChange,
1148
+ helpers,
1149
+ itemFields: itemFieldDefs,
1150
+ itemFieldOrder: fieldDef.itemFieldOrder,
1151
+ minItems,
1152
+ maxItems
1153
+ };
1154
+ } else if (fieldType === "matrix" && fieldDef.type === "matrix") {
1155
+ const matrixValue = baseProps.value ?? null;
1156
+ const rows = fieldDef.rows.map((row) => ({
1157
+ id: row.id,
1158
+ label: row.label,
1159
+ visible: formaVisibility[`${fieldPath}.${row.id}`] !== false
1160
+ }));
1161
+ fieldProps = {
1162
+ ...baseProps,
1163
+ fieldType: "matrix",
1164
+ value: matrixValue,
1165
+ onChange: baseProps.onChange,
1166
+ rows,
1167
+ columns: fieldDef.columns,
1168
+ multiSelect: fieldDef.multiSelect ?? false
1169
+ };
1170
+ } else if (fieldType === "display" && fieldDef.type === "display") {
1171
+ const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
1172
+ const {
1173
+ onChange: _onChange,
1174
+ value: _value,
1175
+ ...displayBaseProps
1176
+ } = baseProps;
1177
+ fieldProps = {
1178
+ ...displayBaseProps,
1179
+ fieldType: "display",
1180
+ content: fieldDef.content,
1181
+ sourceValue,
1182
+ format: fieldDef.format
1183
+ };
1184
+ } else {
1185
+ fieldProps = {
1186
+ ...baseProps,
1187
+ fieldType,
1188
+ value: baseProps.value ?? "",
1189
+ onChange: baseProps.onChange
1190
+ };
1191
+ }
1192
+ const componentProps = { field: fieldProps, spec };
1193
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: /* @__PURE__ */ jsx(
1194
+ FieldWrapper,
1195
+ {
1196
+ fieldPath,
1197
+ field: fieldDef,
1198
+ errors,
1199
+ touched,
1200
+ required,
1201
+ showRequiredIndicator,
1202
+ visible: isVisible,
1203
+ children: React.createElement(
1204
+ Component,
1205
+ componentProps
1206
+ )
1207
+ }
1208
+ ) }, fieldPath);
1209
+ },
1210
+ [
1211
+ spec,
1212
+ components,
1213
+ FieldWrapper,
1214
+ formaData,
1215
+ formaComputed,
1216
+ formaVisibility,
1217
+ formaRequired,
1218
+ formaEnabled,
1219
+ formaReadonly,
1220
+ formaOptionsVisibility,
1221
+ formaTouched,
1222
+ formaErrors,
1223
+ formaIsSubmitted,
1224
+ validateOn,
1225
+ setFieldValue,
1226
+ setFieldTouched,
1227
+ getArrayHelpers
1228
+ ]
1229
+ );
1230
+ const renderedFields = useMemo2(
1231
+ () => fieldsToRender.map(renderField),
1232
+ [fieldsToRender, renderField]
1233
+ );
1234
+ const content = useMemo2(() => {
1235
+ if (spec.pages && spec.pages.length > 0 && forma.wizard) {
1236
+ const currentPage = forma.wizard.currentPage;
1237
+ if (!currentPage) return null;
1238
+ return /* @__PURE__ */ jsx(
1239
+ PageWrapper,
1240
+ {
1241
+ title: currentPage.title,
1242
+ description: currentPage.description,
1243
+ pageIndex: forma.wizard.currentPageIndex,
1244
+ totalPages: forma.wizard.pages.length,
1245
+ children: renderedFields
1246
+ }
1247
+ );
1248
+ }
1249
+ return /* @__PURE__ */ jsx(Fragment, { children: renderedFields });
1250
+ }, [spec.pages, forma.wizard, PageWrapper, renderedFields]);
1251
+ const handleSubmit = useCallback2(
1252
+ (e) => {
1253
+ e == null ? void 0 : e.preventDefault();
1254
+ forma.submitForm();
1255
+ },
1256
+ [forma.submitForm]
1257
+ );
1258
+ return /* @__PURE__ */ jsx(FormaContext.Provider, { value: forma, children: /* @__PURE__ */ jsx(
1259
+ Layout,
1260
+ {
1261
+ onSubmit: handleSubmit,
1262
+ isSubmitting: forma.isSubmitting,
1263
+ isValid: forma.isValid,
1264
+ children: content
1265
+ }
1266
+ ) });
1267
+ }
1268
+ );
1269
+
1270
+ export {
1271
+ useForma,
1272
+ FormaContext,
1273
+ useFormaContext,
1274
+ FormRenderer
1275
+ };
1276
+ //# sourceMappingURL=chunk-5K4QITFH.js.map