@fogpipe/forma-react 0.9.0 → 0.10.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.d.ts +11 -1
- package/dist/index.js +45 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/FormRenderer.tsx +11 -3
- package/src/__tests__/canProceed.test.ts +11 -8
- package/src/__tests__/diabetes-trial-flow.test.ts +41 -24
- package/src/__tests__/null-handling.test.ts +53 -50
- package/src/__tests__/useForma.test.ts +129 -0
- package/src/types.ts +11 -1
- package/src/useForma.ts +28 -1
package/dist/index.d.ts
CHANGED
|
@@ -273,6 +273,11 @@ interface FieldWrapperProps {
|
|
|
273
273
|
errors: FieldError[];
|
|
274
274
|
touched: boolean;
|
|
275
275
|
required: boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Whether to show the required indicator in the UI.
|
|
278
|
+
* False for boolean fields since false is a valid answer.
|
|
279
|
+
*/
|
|
280
|
+
showRequiredIndicator: boolean;
|
|
276
281
|
visible: boolean;
|
|
277
282
|
}
|
|
278
283
|
/**
|
|
@@ -362,8 +367,13 @@ interface GetFieldPropsResult {
|
|
|
362
367
|
visible: boolean;
|
|
363
368
|
/** Whether field is enabled (not disabled) */
|
|
364
369
|
enabled: boolean;
|
|
365
|
-
/** Whether field is required */
|
|
370
|
+
/** Whether field is required (for validation) */
|
|
366
371
|
required: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* Whether to show the required indicator in the UI.
|
|
374
|
+
* False for boolean fields since false is a valid answer.
|
|
375
|
+
*/
|
|
376
|
+
showRequiredIndicator: boolean;
|
|
367
377
|
/** Whether field has been touched */
|
|
368
378
|
touched: boolean;
|
|
369
379
|
/** Validation errors for this field */
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,18 @@ function formReducer(state, action) {
|
|
|
50
50
|
return state;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
function getDefaultBooleanValues(spec) {
|
|
54
|
+
var _a;
|
|
55
|
+
const defaults = {};
|
|
56
|
+
for (const fieldPath of spec.fieldOrder) {
|
|
57
|
+
const schemaProperty = (_a = spec.schema.properties) == null ? void 0 : _a[fieldPath];
|
|
58
|
+
const fieldDef = spec.fields[fieldPath];
|
|
59
|
+
if ((schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean") {
|
|
60
|
+
defaults[fieldPath] = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return defaults;
|
|
64
|
+
}
|
|
53
65
|
function useForma(options) {
|
|
54
66
|
const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
|
|
55
67
|
const spec = useMemo(() => {
|
|
@@ -63,7 +75,8 @@ function useForma(options) {
|
|
|
63
75
|
};
|
|
64
76
|
}, [inputSpec, referenceData]);
|
|
65
77
|
const [state, dispatch] = useReducer(formReducer, {
|
|
66
|
-
data: initialData,
|
|
78
|
+
data: { ...getDefaultBooleanValues(spec), ...initialData },
|
|
79
|
+
// Boolean defaults merged UNDER initialData
|
|
67
80
|
touched: {},
|
|
68
81
|
isSubmitting: false,
|
|
69
82
|
isSubmitted: false,
|
|
@@ -305,23 +318,24 @@ function useForma(options) {
|
|
|
305
318
|
return fieldHandlers.current.get(path);
|
|
306
319
|
}, [setValueAtPath, setFieldTouched]);
|
|
307
320
|
const getFieldProps = useCallback((path) => {
|
|
321
|
+
var _a;
|
|
308
322
|
const fieldDef = spec.fields[path];
|
|
309
323
|
const handlers = getFieldHandlers(path);
|
|
310
324
|
let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
|
|
311
325
|
if (!fieldType || fieldType === "computed") {
|
|
312
|
-
const
|
|
313
|
-
if (
|
|
314
|
-
if (
|
|
315
|
-
else if (
|
|
316
|
-
else if (
|
|
317
|
-
else if (
|
|
318
|
-
else if (
|
|
319
|
-
else if ("enum" in
|
|
320
|
-
else if ("format" in
|
|
321
|
-
if (
|
|
322
|
-
else if (
|
|
323
|
-
else if (
|
|
324
|
-
else if (
|
|
326
|
+
const schemaProperty2 = spec.schema.properties[path];
|
|
327
|
+
if (schemaProperty2) {
|
|
328
|
+
if (schemaProperty2.type === "number") fieldType = "number";
|
|
329
|
+
else if (schemaProperty2.type === "integer") fieldType = "integer";
|
|
330
|
+
else if (schemaProperty2.type === "boolean") fieldType = "boolean";
|
|
331
|
+
else if (schemaProperty2.type === "array") fieldType = "array";
|
|
332
|
+
else if (schemaProperty2.type === "object") fieldType = "object";
|
|
333
|
+
else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
|
|
334
|
+
else if ("format" in schemaProperty2) {
|
|
335
|
+
if (schemaProperty2.format === "date") fieldType = "date";
|
|
336
|
+
else if (schemaProperty2.format === "date-time") fieldType = "datetime";
|
|
337
|
+
else if (schemaProperty2.format === "email") fieldType = "email";
|
|
338
|
+
else if (schemaProperty2.format === "uri") fieldType = "url";
|
|
325
339
|
}
|
|
326
340
|
}
|
|
327
341
|
}
|
|
@@ -331,6 +345,10 @@ function useForma(options) {
|
|
|
331
345
|
const displayedErrors = showErrors ? fieldErrors : [];
|
|
332
346
|
const hasErrors = displayedErrors.length > 0;
|
|
333
347
|
const isRequired = required[path] ?? false;
|
|
348
|
+
const schemaProperty = spec.schema.properties[path];
|
|
349
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
350
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
351
|
+
const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
|
|
334
352
|
return {
|
|
335
353
|
name: path,
|
|
336
354
|
value: getValueAtPath(path),
|
|
@@ -341,6 +359,7 @@ function useForma(options) {
|
|
|
341
359
|
visible: visibility[path] !== false,
|
|
342
360
|
enabled: enabled[path] !== false,
|
|
343
361
|
required: isRequired,
|
|
362
|
+
showRequiredIndicator,
|
|
344
363
|
touched: isTouched,
|
|
345
364
|
errors: displayedErrors,
|
|
346
365
|
onChange: handlers.onChange,
|
|
@@ -387,6 +406,8 @@ function useForma(options) {
|
|
|
387
406
|
enabled: enabled[path] !== false,
|
|
388
407
|
required: false,
|
|
389
408
|
// TODO: Evaluate item field required
|
|
409
|
+
showRequiredIndicator: false,
|
|
410
|
+
// Item fields don't show required indicator
|
|
390
411
|
touched: isTouched,
|
|
391
412
|
errors: showErrors ? fieldErrors : [],
|
|
392
413
|
onChange: handlers.onChange,
|
|
@@ -490,7 +511,7 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
|
|
|
490
511
|
}
|
|
491
512
|
);
|
|
492
513
|
}
|
|
493
|
-
function DefaultFieldWrapper({ fieldPath, field, children, errors,
|
|
514
|
+
function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }) {
|
|
494
515
|
if (!visible) return null;
|
|
495
516
|
const errorId = `${fieldPath}-error`;
|
|
496
517
|
const descriptionId = field.description ? `${fieldPath}-description` : void 0;
|
|
@@ -498,8 +519,8 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, required, vis
|
|
|
498
519
|
return /* @__PURE__ */ jsxs("div", { className: "field-wrapper", "data-field-path": fieldPath, children: [
|
|
499
520
|
field.label && /* @__PURE__ */ jsxs("label", { htmlFor: fieldPath, children: [
|
|
500
521
|
field.label,
|
|
501
|
-
|
|
502
|
-
|
|
522
|
+
showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "required", "aria-hidden": "true", children: "*" }),
|
|
523
|
+
showRequiredIndicator && /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (required)" })
|
|
503
524
|
] }),
|
|
504
525
|
children,
|
|
505
526
|
hasErrors && /* @__PURE__ */ jsx(
|
|
@@ -607,6 +628,7 @@ var FormRenderer = forwardRef(
|
|
|
607
628
|
return spec.fieldOrder;
|
|
608
629
|
}, [spec.pages, spec.fieldOrder, forma.wizard]);
|
|
609
630
|
const renderField = useCallback2((fieldPath) => {
|
|
631
|
+
var _a;
|
|
610
632
|
const fieldDef = spec.fields[fieldPath];
|
|
611
633
|
if (!fieldDef) return null;
|
|
612
634
|
const isVisible = forma.visibility[fieldPath] !== false;
|
|
@@ -623,6 +645,9 @@ var FormRenderer = forwardRef(
|
|
|
623
645
|
const required = forma.required[fieldPath] ?? false;
|
|
624
646
|
const disabled = forma.enabled[fieldPath] === false;
|
|
625
647
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
648
|
+
const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
|
|
649
|
+
const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
|
|
650
|
+
const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
|
|
626
651
|
const baseProps = {
|
|
627
652
|
name: fieldPath,
|
|
628
653
|
field: fieldDef,
|
|
@@ -707,10 +732,10 @@ var FormRenderer = forwardRef(
|
|
|
707
732
|
move: cachedHelpers.move,
|
|
708
733
|
swap: cachedHelpers.swap,
|
|
709
734
|
getItemFieldProps: (index, fieldName) => {
|
|
710
|
-
var
|
|
735
|
+
var _a2;
|
|
711
736
|
const itemFieldDef = itemFieldDefs[fieldName];
|
|
712
737
|
const itemPath = `${fieldPath}[${index}].${fieldName}`;
|
|
713
|
-
const itemValue = (
|
|
738
|
+
const itemValue = (_a2 = arrayValue[index]) == null ? void 0 : _a2[fieldName];
|
|
714
739
|
return {
|
|
715
740
|
name: itemPath,
|
|
716
741
|
value: itemValue,
|
|
@@ -768,6 +793,7 @@ var FormRenderer = forwardRef(
|
|
|
768
793
|
errors,
|
|
769
794
|
touched,
|
|
770
795
|
required,
|
|
796
|
+
showRequiredIndicator,
|
|
771
797
|
visible: isVisible,
|
|
772
798
|
children: React.createElement(Component, componentProps)
|
|
773
799
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useForma.ts","../src/FormRenderer.tsx","../src/context.ts","../src/FieldRenderer.tsx","../src/ErrorBoundary.tsx"],"sourcesContent":["/**\n * useForma Hook\n *\n * Main hook for managing Forma form state.\n * This is a placeholder - the full implementation will be migrated from formidable.\n */\n\nimport { useCallback, useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport type { Forma, FieldError, ValidationResult } from \"@fogpipe/forma-core\";\nimport type { GetFieldPropsResult, GetSelectFieldPropsResult, GetArrayHelpersResult } from \"./types.js\";\nimport {\n getVisibility,\n getRequired,\n getEnabled,\n validate,\n calculate,\n getPageVisibility,\n} from \"@fogpipe/forma-core\";\n\n/**\n * Options for useForma hook\n */\nexport interface UseFormaOptions {\n /** The Forma specification */\n spec: Forma;\n /** Initial form data */\n initialData?: Record<string, unknown>;\n /** Submit handler */\n onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;\n /** Change handler */\n onChange?: (data: Record<string, unknown>, computed?: Record<string, unknown>) => void;\n /** When to validate: on change, blur, or submit only */\n validateOn?: \"change\" | \"blur\" | \"submit\";\n /** Additional reference data to merge with spec.referenceData */\n referenceData?: Record<string, unknown>;\n /**\n * Debounce validation by this many milliseconds.\n * Useful for large forms to improve performance.\n * Set to 0 (default) for immediate validation.\n */\n validationDebounceMs?: number;\n}\n\n/**\n * Form state\n */\ninterface FormState {\n data: Record<string, unknown>;\n touched: Record<string, boolean>;\n isSubmitting: boolean;\n isSubmitted: boolean;\n isDirty: boolean;\n currentPage: number;\n}\n\n/**\n * State actions\n */\ntype FormAction =\n | { type: \"SET_FIELD_VALUE\"; field: string; value: unknown }\n | { type: \"SET_FIELD_TOUCHED\"; field: string; touched: boolean }\n | { type: \"SET_VALUES\"; values: Record<string, unknown> }\n | { type: \"SET_SUBMITTING\"; isSubmitting: boolean }\n | { type: \"SET_SUBMITTED\"; isSubmitted: boolean }\n | { type: \"SET_PAGE\"; page: number }\n | { type: \"RESET\"; initialData: Record<string, unknown> };\n\n/**\n * Page state for multi-page forms\n */\nexport interface PageState {\n id: string;\n title: string;\n description?: string;\n visible: boolean;\n fields: string[];\n}\n\n/**\n * Wizard navigation helpers\n */\nexport interface WizardHelpers {\n pages: PageState[];\n currentPageIndex: number;\n currentPage: PageState | null;\n goToPage: (index: number) => void;\n nextPage: () => void;\n previousPage: () => void;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n canProceed: boolean;\n isLastPage: boolean;\n touchCurrentPageFields: () => void;\n validateCurrentPage: () => boolean;\n}\n\n/**\n * Return type of useForma hook\n */\nexport interface UseFormaReturn {\n /** Current form data */\n data: Record<string, unknown>;\n /** Computed field values */\n computed: Record<string, unknown>;\n /** Field visibility map */\n visibility: Record<string, boolean>;\n /** Field required state map */\n required: Record<string, boolean>;\n /** Field enabled state map */\n enabled: Record<string, boolean>;\n /** Field touched state map */\n touched: Record<string, boolean>;\n /** Validation errors */\n errors: FieldError[];\n /** Whether form is valid */\n isValid: boolean;\n /** Whether form is submitting */\n isSubmitting: boolean;\n /** Whether form has been submitted */\n isSubmitted: boolean;\n /** Whether any field has been modified */\n isDirty: boolean;\n /** The Forma spec */\n spec: Forma;\n /** Wizard helpers (if multi-page) */\n wizard: WizardHelpers | null;\n\n /** Set a field value */\n setFieldValue: (path: string, value: unknown) => void;\n /** Set a field as touched */\n setFieldTouched: (path: string, touched?: boolean) => void;\n /** Set multiple values */\n setValues: (values: Record<string, unknown>) => void;\n /** Validate a single field */\n validateField: (path: string) => FieldError[];\n /** Validate entire form */\n validateForm: () => ValidationResult;\n /** Submit the form */\n submitForm: () => Promise<void>;\n /** Reset the form */\n resetForm: () => void;\n\n // Helper methods for getting field props\n /** Get props for any field */\n getFieldProps: (path: string) => GetFieldPropsResult;\n /** Get props for select field (includes options) */\n getSelectFieldProps: (path: string) => GetSelectFieldPropsResult;\n /** Get array helpers for array field */\n getArrayHelpers: (path: string) => GetArrayHelpersResult;\n}\n\n/**\n * State reducer\n */\nfunction formReducer(state: FormState, action: FormAction): FormState {\n switch (action.type) {\n case \"SET_FIELD_VALUE\":\n return {\n ...state,\n data: { ...state.data, [action.field]: action.value },\n isDirty: true,\n isSubmitted: false, // Clear on data change\n };\n case \"SET_FIELD_TOUCHED\":\n return {\n ...state,\n touched: { ...state.touched, [action.field]: action.touched },\n };\n case \"SET_VALUES\":\n return {\n ...state,\n data: { ...state.data, ...action.values },\n isDirty: true,\n isSubmitted: false, // Clear on data change\n };\n case \"SET_SUBMITTING\":\n return { ...state, isSubmitting: action.isSubmitting };\n case \"SET_SUBMITTED\":\n return { ...state, isSubmitted: action.isSubmitted };\n case \"SET_PAGE\":\n return { ...state, currentPage: action.page };\n case \"RESET\":\n return {\n data: action.initialData,\n touched: {},\n isSubmitting: false,\n isSubmitted: false,\n isDirty: false,\n currentPage: 0,\n };\n default:\n return state;\n }\n}\n\n/**\n * Main Forma hook\n */\nexport function useForma(options: UseFormaOptions): UseFormaReturn {\n const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = \"blur\", referenceData, validationDebounceMs = 0 } = options;\n\n // Merge referenceData from options with spec.referenceData\n const spec = useMemo((): Forma => {\n if (!referenceData) return inputSpec;\n return {\n ...inputSpec,\n referenceData: {\n ...inputSpec.referenceData,\n ...referenceData,\n },\n };\n }, [inputSpec, referenceData]);\n\n const [state, dispatch] = useReducer(formReducer, {\n data: initialData,\n touched: {},\n isSubmitting: false,\n isSubmitted: false,\n isDirty: false,\n currentPage: 0,\n });\n\n // Track if we've initialized (to avoid calling onChange on first render)\n const hasInitialized = useRef(false);\n\n // Calculate computed values\n const computed = useMemo(\n () => calculate(state.data, spec),\n [state.data, spec]\n );\n\n // Calculate visibility\n const visibility = useMemo(\n () => getVisibility(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Calculate required state\n const required = useMemo(\n () => getRequired(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Calculate enabled state\n const enabled = useMemo(\n () => getEnabled(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Validate form - compute immediate result\n const immediateValidation = useMemo(\n () => validate(state.data, spec, { computed, onlyVisible: true }),\n [state.data, spec, computed]\n );\n\n // Debounced validation state (only used when validationDebounceMs > 0)\n const [debouncedValidation, setDebouncedValidation] = useState<ValidationResult>(immediateValidation);\n\n // Apply debouncing if configured\n useEffect(() => {\n if (validationDebounceMs <= 0) {\n // No debouncing - use immediate validation\n setDebouncedValidation(immediateValidation);\n return;\n }\n\n // Debounce validation updates\n const timeoutId = setTimeout(() => {\n setDebouncedValidation(immediateValidation);\n }, validationDebounceMs);\n\n return () => clearTimeout(timeoutId);\n }, [immediateValidation, validationDebounceMs]);\n\n // Use debounced validation for display, but immediate for submit\n const validation = validationDebounceMs > 0 ? debouncedValidation : immediateValidation;\n\n // isDirty is tracked via reducer state for O(1) performance\n\n // Call onChange when data changes (not on initial render)\n useEffect(() => {\n if (hasInitialized.current) {\n onChange?.(state.data, computed);\n } else {\n hasInitialized.current = true;\n }\n }, [state.data, computed, onChange]);\n\n // Helper function to set value at nested path\n const setNestedValue = useCallback((path: string, value: unknown): void => {\n // Handle array index notation: \"items[0].name\" -> nested structure\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n\n if (parts.length === 1) {\n // Simple path - just set directly\n dispatch({ type: \"SET_FIELD_VALUE\", field: path, value });\n return;\n }\n\n // Build nested object for complex paths\n const buildNestedObject = (data: Record<string, unknown>, pathParts: string[], val: unknown): Record<string, unknown> => {\n const result = { ...data };\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < pathParts.length - 1; i++) {\n const part = pathParts[i];\n const nextPart = pathParts[i + 1];\n const isNextArrayIndex = /^\\d+$/.test(nextPart);\n\n if (current[part] === undefined) {\n current[part] = isNextArrayIndex ? [] : {};\n } else if (Array.isArray(current[part])) {\n current[part] = [...(current[part] as unknown[])];\n } else {\n current[part] = { ...(current[part] as Record<string, unknown>) };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[pathParts[pathParts.length - 1]] = val;\n return result;\n };\n\n dispatch({ type: \"SET_VALUES\", values: buildNestedObject(state.data, parts, value) });\n }, [state.data]);\n\n // Actions\n const setFieldValue = useCallback(\n (path: string, value: unknown) => {\n setNestedValue(path, value);\n if (validateOn === \"change\") {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field: path, touched: true });\n }\n },\n [validateOn, setNestedValue]\n );\n\n const setFieldTouched = useCallback((path: string, touched = true) => {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field: path, touched });\n }, []);\n\n const setValues = useCallback((values: Record<string, unknown>) => {\n dispatch({ type: \"SET_VALUES\", values });\n }, []);\n\n const validateField = useCallback(\n (path: string): FieldError[] => {\n return validation.errors.filter((e) => e.field === path);\n },\n [validation]\n );\n\n const validateForm = useCallback((): ValidationResult => {\n return validation;\n }, [validation]);\n\n const submitForm = useCallback(async () => {\n dispatch({ type: \"SET_SUBMITTING\", isSubmitting: true });\n try {\n // Always use immediate validation on submit to ensure accurate result\n if (immediateValidation.valid && onSubmit) {\n await onSubmit(state.data);\n }\n dispatch({ type: \"SET_SUBMITTED\", isSubmitted: true });\n } finally {\n dispatch({ type: \"SET_SUBMITTING\", isSubmitting: false });\n }\n }, [immediateValidation, onSubmit, state.data]);\n\n const resetForm = useCallback(() => {\n dispatch({ type: \"RESET\", initialData });\n }, [initialData]);\n\n // Wizard helpers\n const wizard = useMemo((): WizardHelpers | null => {\n if (!spec.pages || spec.pages.length === 0) return null;\n\n const pageVisibility = getPageVisibility(state.data, spec, { computed });\n\n // Include all pages with their visibility status\n const pages: PageState[] = spec.pages.map((p) => ({\n id: p.id,\n title: p.title,\n description: p.description,\n visible: pageVisibility[p.id] !== false,\n fields: p.fields,\n }));\n\n // For navigation, only count visible pages\n const visiblePages = pages.filter((p) => p.visible);\n\n // Clamp currentPage to valid range (handles case where current page becomes hidden)\n const maxPageIndex = Math.max(0, visiblePages.length - 1);\n const clampedPageIndex = Math.min(Math.max(0, state.currentPage), maxPageIndex);\n\n // Auto-correct page index if it's out of bounds\n if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex });\n }\n\n const currentPage = visiblePages[clampedPageIndex] || null;\n const hasNextPage = clampedPageIndex < visiblePages.length - 1;\n const hasPreviousPage = clampedPageIndex > 0;\n const isLastPage = clampedPageIndex === visiblePages.length - 1;\n\n return {\n pages,\n currentPageIndex: clampedPageIndex,\n currentPage,\n goToPage: (index: number) => {\n // Clamp to valid range\n const validIndex = Math.min(Math.max(0, index), maxPageIndex);\n dispatch({ type: \"SET_PAGE\", page: validIndex });\n },\n nextPage: () => {\n if (hasNextPage) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex + 1 });\n }\n },\n previousPage: () => {\n if (hasPreviousPage) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex - 1 });\n }\n },\n hasNextPage,\n hasPreviousPage,\n canProceed: (() => {\n if (!currentPage) return true;\n // Get errors only for visible fields on the current page\n const pageErrors = validation.errors.filter((e) => {\n // Check if field is on current page (including array items like \"items[0].name\")\n const isOnCurrentPage = currentPage.fields.includes(e.field) ||\n currentPage.fields.some(f => e.field.startsWith(`${f}[`));\n // Only count errors for visible fields\n const isVisible = visibility[e.field] !== false;\n // Only count actual errors, not warnings\n const isError = e.severity === 'error';\n return isOnCurrentPage && isVisible && isError;\n });\n return pageErrors.length === 0;\n })(),\n isLastPage,\n touchCurrentPageFields: () => {\n if (currentPage) {\n currentPage.fields.forEach((field) => {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field, touched: true });\n });\n }\n },\n validateCurrentPage: () => {\n if (!currentPage) return true;\n const pageErrors = validation.errors.filter((e) =>\n currentPage.fields.includes(e.field)\n );\n return pageErrors.length === 0;\n },\n };\n }, [spec, state.data, state.currentPage, computed, validation, visibility]);\n\n // Helper to get value at nested path\n const getValueAtPath = useCallback((path: string): unknown => {\n // Handle array index notation: \"items[0].name\" -> [\"items\", \"0\", \"name\"]\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n let value: unknown = state.data;\n for (const part of parts) {\n if (value === null || value === undefined) return undefined;\n value = (value as Record<string, unknown>)[part];\n }\n return value;\n }, [state.data]);\n\n // Helper to set value at nested path\n const setValueAtPath = useCallback((path: string, value: unknown): void => {\n // For nested paths, we need to build the nested structure\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n if (parts.length === 1) {\n dispatch({ type: \"SET_FIELD_VALUE\", field: path, value });\n return;\n }\n\n // Build nested object\n const newData = { ...state.data };\n let current: Record<string, unknown> = newData;\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n const nextPart = parts[i + 1];\n const isNextArrayIndex = /^\\d+$/.test(nextPart);\n\n if (current[part] === undefined) {\n current[part] = isNextArrayIndex ? [] : {};\n } else if (Array.isArray(current[part])) {\n current[part] = [...(current[part] as unknown[])];\n } else {\n current[part] = { ...(current[part] as Record<string, unknown>) };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n dispatch({ type: \"SET_VALUES\", values: newData });\n }, [state.data]);\n\n // Memoized onChange/onBlur handlers for fields\n const fieldHandlers = useRef<Map<string, { onChange: (value: unknown) => void; onBlur: () => void }>>(new Map());\n\n // Clean up stale field handlers when spec changes to prevent memory leaks\n useEffect(() => {\n const validFields = new Set(spec.fieldOrder);\n // Also include array item field patterns\n for (const fieldId of spec.fieldOrder) {\n const fieldDef = spec.fields[fieldId];\n if (fieldDef?.itemFields) {\n for (const key of fieldHandlers.current.keys()) {\n if (key.startsWith(`${fieldId}[`)) {\n validFields.add(key);\n }\n }\n }\n }\n // Remove handlers for fields that no longer exist\n for (const key of fieldHandlers.current.keys()) {\n const baseField = key.split('[')[0];\n if (!validFields.has(key) && !validFields.has(baseField)) {\n fieldHandlers.current.delete(key);\n }\n }\n }, [spec]);\n\n const getFieldHandlers = useCallback((path: string) => {\n if (!fieldHandlers.current.has(path)) {\n fieldHandlers.current.set(path, {\n onChange: (value: unknown) => setValueAtPath(path, value),\n onBlur: () => setFieldTouched(path),\n });\n }\n return fieldHandlers.current.get(path)!;\n }, [setValueAtPath, setFieldTouched]);\n\n // Get field props for any field\n const getFieldProps = useCallback((path: string): GetFieldPropsResult => {\n const fieldDef = spec.fields[path];\n const handlers = getFieldHandlers(path);\n\n // Determine field type from definition or infer from schema\n let fieldType = fieldDef?.type || \"text\";\n if (!fieldType || fieldType === \"computed\") {\n const schemaProperty = spec.schema.properties[path];\n if (schemaProperty) {\n if (schemaProperty.type === \"number\") fieldType = \"number\";\n else if (schemaProperty.type === \"integer\") fieldType = \"integer\";\n else if (schemaProperty.type === \"boolean\") fieldType = \"boolean\";\n else if (schemaProperty.type === \"array\") fieldType = \"array\";\n else if (schemaProperty.type === \"object\") fieldType = \"object\";\n else if (\"enum\" in schemaProperty && schemaProperty.enum) fieldType = \"select\";\n else if (\"format\" in schemaProperty) {\n if (schemaProperty.format === \"date\") fieldType = \"date\";\n else if (schemaProperty.format === \"date-time\") fieldType = \"datetime\";\n else if (schemaProperty.format === \"email\") fieldType = \"email\";\n else if (schemaProperty.format === \"uri\") fieldType = \"url\";\n }\n }\n }\n\n const fieldErrors = validation.errors.filter((e) => e.field === path);\n const isTouched = state.touched[path] ?? false;\n const showErrors = validateOn === \"change\" || (validateOn === \"blur\" && isTouched) || state.isSubmitted;\n const displayedErrors = showErrors ? fieldErrors : [];\n const hasErrors = displayedErrors.length > 0;\n const isRequired = required[path] ?? false;\n\n return {\n name: path,\n value: getValueAtPath(path),\n type: fieldType,\n label: fieldDef?.label || path.charAt(0).toUpperCase() + path.slice(1),\n description: fieldDef?.description,\n placeholder: fieldDef?.placeholder,\n visible: visibility[path] !== false,\n enabled: enabled[path] !== false,\n required: isRequired,\n touched: isTouched,\n errors: displayedErrors,\n onChange: handlers.onChange,\n onBlur: handlers.onBlur,\n // ARIA accessibility attributes\n \"aria-invalid\": hasErrors || undefined,\n \"aria-describedby\": hasErrors ? `${path}-error` : undefined,\n \"aria-required\": isRequired || undefined,\n };\n }, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);\n\n // Get select field props\n const getSelectFieldProps = useCallback((path: string): GetSelectFieldPropsResult => {\n const baseProps = getFieldProps(path);\n const fieldDef = spec.fields[path];\n\n return {\n ...baseProps,\n options: fieldDef?.options ?? [],\n };\n }, [getFieldProps, spec.fields]);\n\n // Get array helpers\n const getArrayHelpers = useCallback((path: string): GetArrayHelpersResult => {\n const fieldDef = spec.fields[path];\n const currentValue = (getValueAtPath(path) as unknown[]) ?? [];\n const minItems = fieldDef?.minItems ?? 0;\n const maxItems = fieldDef?.maxItems ?? Infinity;\n\n const canAdd = currentValue.length < maxItems;\n const canRemove = currentValue.length > minItems;\n\n const getItemFieldProps = (index: number, fieldName: string): GetFieldPropsResult => {\n const itemPath = `${path}[${index}].${fieldName}`;\n const itemFieldDef = fieldDef?.itemFields?.[fieldName];\n const handlers = getFieldHandlers(itemPath);\n\n // Get item value\n const item = currentValue[index] as Record<string, unknown> | undefined;\n const itemValue = item?.[fieldName];\n\n const fieldErrors = validation.errors.filter((e) => e.field === itemPath);\n const isTouched = state.touched[itemPath] ?? false;\n const showErrors = validateOn === \"change\" || (validateOn === \"blur\" && isTouched) || state.isSubmitted;\n\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type || \"text\",\n label: itemFieldDef?.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: enabled[path] !== false,\n required: false, // TODO: Evaluate item field required\n touched: isTouched,\n errors: showErrors ? fieldErrors : [],\n onChange: handlers.onChange,\n onBlur: handlers.onBlur,\n };\n };\n\n return {\n items: currentValue,\n push: (item: unknown) => {\n if (canAdd) {\n setValueAtPath(path, [...currentValue, item]);\n }\n },\n remove: (index: number) => {\n if (canRemove) {\n const newArray = [...currentValue];\n newArray.splice(index, 1);\n setValueAtPath(path, newArray);\n }\n },\n move: (from: number, to: number) => {\n const newArray = [...currentValue];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n setValueAtPath(path, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const newArray = [...currentValue];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n setValueAtPath(path, newArray);\n },\n insert: (index: number, item: unknown) => {\n if (canAdd) {\n const newArray = [...currentValue];\n newArray.splice(index, 0, item);\n setValueAtPath(path, newArray);\n }\n },\n getItemFieldProps,\n minItems,\n maxItems,\n canAdd,\n canRemove,\n };\n }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn]);\n\n return {\n data: state.data,\n computed,\n visibility,\n required,\n enabled,\n touched: state.touched,\n errors: validation.errors,\n isValid: validation.valid,\n isSubmitting: state.isSubmitting,\n isSubmitted: state.isSubmitted,\n isDirty: state.isDirty,\n spec,\n wizard,\n setFieldValue,\n setFieldTouched,\n setValues,\n validateField,\n validateForm,\n submitForm,\n resetForm,\n getFieldProps,\n getSelectFieldProps,\n getArrayHelpers,\n };\n}\n","/**\n * FormRenderer Component\n *\n * Renders a complete form from a Forma specification.\n * Supports single-page and multi-page (wizard) forms.\n */\n\nimport React, { forwardRef, useImperativeHandle, useRef, useMemo, useCallback } from \"react\";\nimport type { Forma, FieldDefinition, ValidationResult, JSONSchemaProperty } from \"@fogpipe/forma-core\";\nimport { useForma } from \"./useForma.js\";\nimport { FormaContext } from \"./context.js\";\nimport type { ComponentMap, LayoutProps, FieldWrapperProps, PageWrapperProps, BaseFieldProps, TextFieldProps, NumberFieldProps, SelectFieldProps, ArrayFieldProps, ArrayHelpers } from \"./types.js\";\n\n/**\n * Props for FormRenderer component\n */\nexport interface FormRendererProps {\n /** The Forma specification */\n spec: Forma;\n /** Initial form data */\n initialData?: Record<string, unknown>;\n /** Submit handler */\n onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;\n /** Change handler */\n onChange?: (data: Record<string, unknown>, computed?: Record<string, unknown>) => void;\n /** Component map for rendering fields */\n components: ComponentMap;\n /** Custom layout component */\n layout?: React.ComponentType<LayoutProps>;\n /** Custom field wrapper component */\n fieldWrapper?: React.ComponentType<FieldWrapperProps>;\n /** Custom page wrapper component */\n pageWrapper?: React.ComponentType<PageWrapperProps>;\n /** When to validate */\n validateOn?: \"change\" | \"blur\" | \"submit\";\n /** Current page for controlled wizard */\n page?: number;\n}\n\n/**\n * Imperative handle for FormRenderer\n */\nexport interface FormRendererHandle {\n submitForm: () => Promise<void>;\n resetForm: () => void;\n validateForm: () => ValidationResult;\n focusField: (path: string) => void;\n focusFirstError: () => void;\n getValues: () => Record<string, unknown>;\n setValues: (values: Record<string, unknown>) => void;\n isValid: boolean;\n isDirty: boolean;\n}\n\n/**\n * Default layout component\n */\nfunction DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n onSubmit();\n }}\n >\n {children}\n <button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting ? \"Submitting...\" : \"Submit\"}\n </button>\n </form>\n );\n}\n\n/**\n * Default field wrapper component with accessibility support\n */\nfunction DefaultFieldWrapper({ fieldPath, field, children, errors, required, visible }: FieldWrapperProps) {\n if (!visible) return null;\n\n const errorId = `${fieldPath}-error`;\n const descriptionId = field.description ? `${fieldPath}-description` : undefined;\n const hasErrors = errors.length > 0;\n\n return (\n <div className=\"field-wrapper\" data-field-path={fieldPath}>\n {field.label && (\n <label htmlFor={fieldPath}>\n {field.label}\n {required && <span className=\"required\" aria-hidden=\"true\">*</span>}\n {required && <span className=\"sr-only\"> (required)</span>}\n </label>\n )}\n {children}\n {hasErrors && (\n <div\n id={errorId}\n className=\"field-errors\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n {errors.map((error, i) => (\n <span key={i} className=\"error\">\n {error.message}\n </span>\n ))}\n </div>\n )}\n {field.description && (\n <p id={descriptionId} className=\"field-description\">\n {field.description}\n </p>\n )}\n </div>\n );\n}\n\n/**\n * Default page wrapper component\n */\nfunction DefaultPageWrapper({ title, description, children }: PageWrapperProps) {\n return (\n <div className=\"page-wrapper\">\n <h2>{title}</h2>\n {description && <p>{description}</p>}\n {children}\n </div>\n );\n}\n\n/**\n * Extract numeric constraints from JSON Schema property\n */\nfunction getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?: number; step?: number } {\n if (!schema) return {};\n if (schema.type !== \"number\" && schema.type !== \"integer\") return {};\n\n // Extract min/max from schema\n const min = \"minimum\" in schema && typeof schema.minimum === \"number\" ? schema.minimum : undefined;\n const max = \"maximum\" in schema && typeof schema.maximum === \"number\" ? schema.maximum : undefined;\n\n // Use multipleOf for step if defined, otherwise default to 1 for integers\n let step: number | undefined;\n if (\"multipleOf\" in schema && typeof schema.multipleOf === \"number\") {\n step = schema.multipleOf;\n } else if (schema.type === \"integer\") {\n step = 1;\n }\n\n return { min, max, step };\n}\n\n/**\n * Create a default item for an array field based on item field definitions\n */\nfunction createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {\n const item: Record<string, unknown> = {};\n for (const [fieldName, fieldDef] of Object.entries(itemFields)) {\n if (fieldDef.type === \"boolean\") {\n item[fieldName] = false;\n } else if (fieldDef.type === \"number\" || fieldDef.type === \"integer\") {\n item[fieldName] = null;\n } else {\n item[fieldName] = \"\";\n }\n }\n return item;\n}\n\n/**\n * FormRenderer component\n */\nexport const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(\n function FormRenderer(props, ref) {\n const {\n spec,\n initialData,\n onSubmit,\n onChange,\n components,\n layout: Layout = DefaultLayout,\n fieldWrapper: FieldWrapper = DefaultFieldWrapper,\n pageWrapper: PageWrapper = DefaultPageWrapper,\n validateOn,\n } = props;\n\n const forma = useForma({\n spec,\n initialData,\n onSubmit,\n onChange,\n validateOn,\n });\n\n const fieldRefs = useRef<Map<string, HTMLElement>>(new Map());\n\n // Cache for array helper functions to prevent recreation on every render\n const arrayHelpersCache = useRef<Map<string, {\n push: (item?: unknown) => void;\n insert: (index: number, item: unknown) => void;\n remove: (index: number) => void;\n move: (from: number, to: number) => void;\n swap: (indexA: number, indexB: number) => void;\n }>>(new Map());\n\n // Focus a specific field by path\n const focusField = useCallback((path: string) => {\n const element = fieldRefs.current.get(path);\n element?.focus();\n }, []);\n\n // Focus the first field with an error\n const focusFirstError = useCallback(() => {\n const firstError = forma.errors[0];\n if (firstError) {\n focusField(firstError.field);\n }\n }, [forma.errors, focusField]);\n\n // Expose imperative handle\n useImperativeHandle(\n ref,\n () => ({\n submitForm: forma.submitForm,\n resetForm: forma.resetForm,\n validateForm: forma.validateForm,\n focusField,\n focusFirstError,\n getValues: () => forma.data,\n setValues: forma.setValues,\n isValid: forma.isValid,\n isDirty: forma.isDirty,\n }),\n [forma, focusField, focusFirstError]\n );\n\n // Determine which fields to render based on pages or fieldOrder\n const fieldsToRender = useMemo(() => {\n if (spec.pages && spec.pages.length > 0 && forma.wizard) {\n // Wizard mode - render fields for the active page\n const currentPage = forma.wizard.currentPage;\n if (currentPage) {\n return currentPage.fields;\n }\n // Fallback to first page\n return spec.pages[0]?.fields ?? [];\n }\n // Single page mode - render all fields in order\n return spec.fieldOrder;\n }, [spec.pages, spec.fieldOrder, forma.wizard]);\n\n // Render a single field (memoized)\n const renderField = useCallback((fieldPath: string) => {\n const fieldDef = spec.fields[fieldPath];\n if (!fieldDef) return null;\n\n const isVisible = forma.visibility[fieldPath] !== false;\n if (!isVisible) return null;\n\n // Infer field type\n const fieldType = fieldDef.type || (fieldDef.itemFields ? \"array\" : \"text\");\n const componentKey = fieldType as keyof ComponentMap;\n const Component = components[componentKey] || components.fallback;\n\n if (!Component) {\n console.warn(`No component found for field type: ${fieldType}`);\n return null;\n }\n\n const errors = forma.errors.filter((e) => e.field === fieldPath);\n const touched = forma.touched[fieldPath] ?? false;\n const required = forma.required[fieldPath] ?? false;\n const disabled = forma.enabled[fieldPath] === false;\n\n // Get schema property for additional constraints\n const schemaProperty = spec.schema.properties[fieldPath];\n\n // Base field props\n const baseProps: BaseFieldProps = {\n name: fieldPath,\n field: fieldDef,\n value: forma.data[fieldPath],\n touched,\n required,\n disabled,\n errors,\n onChange: (value: unknown) => forma.setFieldValue(fieldPath, value),\n onBlur: () => forma.setFieldTouched(fieldPath),\n // Convenience properties\n visible: true, // Always true since we already filtered for visibility\n enabled: !disabled,\n label: fieldDef.label ?? fieldPath,\n description: fieldDef.description,\n placeholder: fieldDef.placeholder,\n };\n\n // Build type-specific props\n let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | SelectFieldProps | ArrayFieldProps = baseProps;\n\n if (fieldType === \"number\" || fieldType === \"integer\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType,\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n ...constraints,\n } as NumberFieldProps;\n } else if (fieldType === \"select\" || fieldType === \"multiselect\") {\n fieldProps = {\n ...baseProps,\n fieldType,\n value: baseProps.value as string | string[] | null,\n onChange: baseProps.onChange as (value: string | string[] | null) => void,\n options: fieldDef.options ?? [],\n } as SelectFieldProps;\n } else if (fieldType === \"array\" && fieldDef.itemFields) {\n const arrayValue = (baseProps.value as unknown[] | undefined) ?? [];\n const minItems = fieldDef.minItems ?? 0;\n const maxItems = fieldDef.maxItems ?? Infinity;\n const itemFieldDefs = fieldDef.itemFields;\n\n // Get or create cached helper functions for this array field\n // These functions read current values when called, not when created\n if (!arrayHelpersCache.current.has(fieldPath)) {\n arrayHelpersCache.current.set(fieldPath, {\n push: (item?: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newItem = item ?? createDefaultItem(itemFieldDefs);\n forma.setFieldValue(fieldPath, [...currentArray, newItem]);\n },\n insert: (index: number, item: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n newArray.splice(index, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n remove: (index: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n newArray.splice(index, 1);\n forma.setFieldValue(fieldPath, newArray);\n },\n move: (from: number, to: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n forma.setFieldValue(fieldPath, newArray);\n },\n });\n }\n const cachedHelpers = arrayHelpersCache.current.get(fieldPath)!;\n\n const helpers: ArrayHelpers = {\n items: arrayValue,\n push: cachedHelpers.push,\n insert: cachedHelpers.insert,\n remove: cachedHelpers.remove,\n move: cachedHelpers.move,\n swap: cachedHelpers.swap,\n getItemFieldProps: (index: number, fieldName: string) => {\n const itemFieldDef = itemFieldDefs[fieldName];\n const itemPath = `${fieldPath}[${index}].${fieldName}`;\n const itemValue = (arrayValue[index] as Record<string, unknown>)?.[fieldName];\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type ?? \"text\",\n label: itemFieldDef?.label ?? fieldName,\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: !disabled,\n required: itemFieldDef?.requiredWhen === \"true\",\n touched: forma.touched[itemPath] ?? false,\n errors: forma.errors.filter((e) => e.field === itemPath),\n onChange: (value: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n const item = (newArray[index] ?? {}) as Record<string, unknown>;\n newArray[index] = { ...item, [fieldName]: value };\n forma.setFieldValue(fieldPath, newArray);\n },\n onBlur: () => forma.setFieldTouched(itemPath),\n itemIndex: index,\n fieldName,\n options: itemFieldDef?.options,\n };\n },\n minItems,\n maxItems,\n canAdd: arrayValue.length < maxItems,\n canRemove: arrayValue.length > minItems,\n };\n fieldProps = {\n ...baseProps,\n fieldType: \"array\",\n value: arrayValue,\n onChange: baseProps.onChange as (value: unknown[]) => void,\n helpers,\n itemFields: itemFieldDefs,\n minItems,\n maxItems,\n } as ArrayFieldProps;\n } else {\n // Text-based fields\n fieldProps = {\n ...baseProps,\n fieldType: fieldType as \"text\" | \"email\" | \"password\" | \"url\" | \"textarea\",\n value: (baseProps.value as string) ?? \"\",\n onChange: baseProps.onChange as (value: string) => void,\n };\n }\n\n // Wrap props in { field, spec } structure for components\n const componentProps = { field: fieldProps, spec };\n\n return (\n <FieldWrapper\n key={fieldPath}\n fieldPath={fieldPath}\n field={fieldDef}\n errors={errors}\n touched={touched}\n required={required}\n visible={isVisible}\n >\n {React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps)}\n </FieldWrapper>\n );\n }, [spec, forma, components, FieldWrapper]);\n\n // Render fields (memoized)\n const renderedFields = useMemo(\n () => fieldsToRender.map(renderField),\n [fieldsToRender, renderField]\n );\n\n // Render with page wrapper if using pages\n const content = useMemo(() => {\n if (spec.pages && spec.pages.length > 0 && forma.wizard) {\n const currentPage = forma.wizard.currentPage;\n if (!currentPage) return null;\n\n return (\n <PageWrapper\n title={currentPage.title}\n description={currentPage.description}\n pageIndex={forma.wizard.currentPageIndex}\n totalPages={forma.wizard.pages.length}\n >\n {renderedFields}\n </PageWrapper>\n );\n }\n\n return <>{renderedFields}</>;\n }, [spec.pages, forma.wizard, PageWrapper, renderedFields]);\n\n return (\n <FormaContext.Provider value={forma}>\n <Layout\n onSubmit={forma.submitForm}\n isSubmitting={forma.isSubmitting}\n isValid={forma.isValid}\n >\n {content}\n </Layout>\n </FormaContext.Provider>\n );\n }\n);\n","/**\n * React Context for Forma\n */\n\nimport { createContext, useContext } from \"react\";\nimport type { UseFormaReturn } from \"./useForma.js\";\n\n/**\n * Context for sharing form state across components\n */\nexport const FormaContext = createContext<UseFormaReturn | null>(null);\n\n/**\n * Hook to access Forma context\n * @throws Error if used outside of FormaContext.Provider\n */\nexport function useFormaContext(): UseFormaReturn {\n const context = useContext(FormaContext);\n if (!context) {\n throw new Error(\"useFormaContext must be used within a FormaContext.Provider\");\n }\n return context;\n}\n","/**\n * FieldRenderer Component\n *\n * Routes a single field to the appropriate component based on its type.\n * This is useful for custom form layouts where you need field-by-field control.\n */\n\nimport React from \"react\";\nimport type { FieldDefinition, JSONSchemaProperty } from \"@fogpipe/forma-core\";\nimport { useFormaContext } from \"./context.js\";\nimport type {\n ComponentMap,\n BaseFieldProps,\n TextFieldProps,\n NumberFieldProps,\n IntegerFieldProps,\n SelectFieldProps,\n MultiSelectFieldProps,\n ArrayFieldProps,\n ArrayHelpers,\n} from \"./types.js\";\n\n/**\n * Props for FieldRenderer component\n */\nexport interface FieldRendererProps {\n /** Field path (e.g., \"firstName\" or \"address.city\") */\n fieldPath: string;\n /** Component map for rendering fields */\n components: ComponentMap;\n /** Optional class name for the wrapper */\n className?: string;\n}\n\n/**\n * Extract numeric constraints from JSON Schema property\n */\nfunction getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?: number; step?: number } {\n if (!schema) return {};\n if (schema.type !== \"number\" && schema.type !== \"integer\") return {};\n\n // Extract min/max from schema\n const min = \"minimum\" in schema && typeof schema.minimum === \"number\" ? schema.minimum : undefined;\n const max = \"maximum\" in schema && typeof schema.maximum === \"number\" ? schema.maximum : undefined;\n\n // Use multipleOf for step if defined, otherwise default to 1 for integers\n let step: number | undefined;\n if (\"multipleOf\" in schema && typeof schema.multipleOf === \"number\") {\n step = schema.multipleOf;\n } else if (schema.type === \"integer\") {\n step = 1;\n }\n\n return { min, max, step };\n}\n\n/**\n * Create a default item for an array field based on item field definitions\n */\nfunction createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {\n const item: Record<string, unknown> = {};\n for (const [fieldName, fieldDef] of Object.entries(itemFields)) {\n if (fieldDef.type === \"boolean\") {\n item[fieldName] = false;\n } else if (fieldDef.type === \"number\" || fieldDef.type === \"integer\") {\n item[fieldName] = null;\n } else {\n item[fieldName] = \"\";\n }\n }\n return item;\n}\n\n/**\n * FieldRenderer component\n *\n * @example\n * ```tsx\n * // Render a specific field with custom components\n * <FieldRenderer fieldPath=\"email\" components={componentMap} />\n * ```\n */\nexport function FieldRenderer({ fieldPath, components, className }: FieldRendererProps) {\n const forma = useFormaContext();\n const { spec } = forma;\n\n const fieldDef = spec.fields[fieldPath];\n if (!fieldDef) {\n console.warn(`Field not found: ${fieldPath}`);\n return null;\n }\n\n const isVisible = forma.visibility[fieldPath] !== false;\n if (!isVisible) return null;\n\n // Infer field type\n const fieldType = fieldDef.type || (fieldDef.itemFields ? \"array\" : \"text\");\n const componentKey = fieldType as keyof ComponentMap;\n const Component = components[componentKey] || components.fallback;\n\n if (!Component) {\n console.warn(`No component found for field type: ${fieldType}`);\n return null;\n }\n\n const errors = forma.errors.filter((e) => e.field === fieldPath);\n const touched = forma.touched[fieldPath] ?? false;\n const required = forma.required[fieldPath] ?? false;\n const disabled = forma.enabled[fieldPath] === false;\n\n // Get schema property for additional constraints\n const schemaProperty = spec.schema.properties[fieldPath];\n\n // Base field props\n const baseProps: BaseFieldProps = {\n name: fieldPath,\n field: fieldDef,\n value: forma.data[fieldPath],\n touched,\n required,\n disabled,\n errors,\n onChange: (value: unknown) => forma.setFieldValue(fieldPath, value),\n onBlur: () => forma.setFieldTouched(fieldPath),\n // Convenience properties\n visible: true, // Always true since we already filtered for visibility\n enabled: !disabled,\n label: fieldDef.label ?? fieldPath,\n description: fieldDef.description,\n placeholder: fieldDef.placeholder,\n };\n\n // Build type-specific props\n let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | IntegerFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps = baseProps;\n\n if (fieldType === \"number\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType: \"number\",\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n ...constraints,\n } as NumberFieldProps;\n } else if (fieldType === \"integer\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType: \"integer\",\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n min: constraints.min,\n max: constraints.max,\n } as IntegerFieldProps;\n } else if (fieldType === \"select\") {\n fieldProps = {\n ...baseProps,\n fieldType: \"select\",\n value: baseProps.value as string | null,\n onChange: baseProps.onChange as (value: string | null) => void,\n options: fieldDef.options ?? [],\n } as SelectFieldProps;\n } else if (fieldType === \"multiselect\") {\n fieldProps = {\n ...baseProps,\n fieldType: \"multiselect\",\n value: (baseProps.value as string[] | undefined) ?? [],\n onChange: baseProps.onChange as (value: string[]) => void,\n options: fieldDef.options ?? [],\n } as MultiSelectFieldProps;\n } else if (fieldType === \"array\" && fieldDef.itemFields) {\n const arrayValue = (baseProps.value as unknown[] | undefined) ?? [];\n const minItems = fieldDef.minItems ?? 0;\n const maxItems = fieldDef.maxItems ?? Infinity;\n const itemFieldDefs = fieldDef.itemFields;\n\n const helpers: ArrayHelpers = {\n items: arrayValue,\n push: (item?: unknown) => {\n const newItem = item ?? createDefaultItem(itemFieldDefs);\n forma.setFieldValue(fieldPath, [...arrayValue, newItem]);\n },\n insert: (index: number, item: unknown) => {\n const newArray = [...arrayValue];\n newArray.splice(index, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n remove: (index: number) => {\n const newArray = [...arrayValue];\n newArray.splice(index, 1);\n forma.setFieldValue(fieldPath, newArray);\n },\n move: (from: number, to: number) => {\n const newArray = [...arrayValue];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const newArray = [...arrayValue];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n forma.setFieldValue(fieldPath, newArray);\n },\n getItemFieldProps: (index: number, fieldName: string) => {\n const itemFieldDef = itemFieldDefs[fieldName];\n const itemPath = `${fieldPath}[${index}].${fieldName}`;\n const itemValue = (arrayValue[index] as Record<string, unknown>)?.[fieldName];\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type ?? \"text\",\n label: itemFieldDef?.label ?? fieldName,\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: !disabled,\n required: itemFieldDef?.requiredWhen === \"true\",\n touched: forma.touched[itemPath] ?? false,\n errors: forma.errors.filter((e) => e.field === itemPath),\n onChange: (value: unknown) => {\n const newArray = [...arrayValue];\n const item = (newArray[index] ?? {}) as Record<string, unknown>;\n newArray[index] = { ...item, [fieldName]: value };\n forma.setFieldValue(fieldPath, newArray);\n },\n onBlur: () => forma.setFieldTouched(itemPath),\n itemIndex: index,\n fieldName,\n options: itemFieldDef?.options,\n };\n },\n minItems,\n maxItems,\n canAdd: arrayValue.length < maxItems,\n canRemove: arrayValue.length > minItems,\n };\n fieldProps = {\n ...baseProps,\n fieldType: \"array\",\n value: arrayValue,\n onChange: baseProps.onChange as (value: unknown[]) => void,\n helpers,\n itemFields: itemFieldDefs,\n minItems,\n maxItems,\n } as ArrayFieldProps;\n } else {\n // Text-based fields\n fieldProps = {\n ...baseProps,\n fieldType: fieldType as \"text\" | \"email\" | \"password\" | \"url\" | \"textarea\",\n value: (baseProps.value as string) ?? \"\",\n onChange: baseProps.onChange as (value: string) => void,\n };\n }\n\n // Wrap props in { field, spec } structure for components\n const componentProps = { field: fieldProps, spec };\n const element = React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps);\n\n if (className) {\n return <div className={className}>{element}</div>;\n }\n\n return element;\n}\n","/**\n * FormaErrorBoundary Component\n *\n * Error boundary for catching render errors in form components.\n * Provides graceful error handling and recovery options.\n */\n\nimport React from \"react\";\n\n/**\n * Props for FormaErrorBoundary component\n */\nexport interface FormaErrorBoundaryProps {\n /** Child components to render */\n children: React.ReactNode;\n /** Custom fallback UI to show when an error occurs */\n fallback?: React.ReactNode | ((error: Error, reset: () => void) => React.ReactNode);\n /** Callback when an error is caught */\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n /** Key to reset the error boundary (change this to reset) */\n resetKey?: string | number;\n}\n\n/**\n * State for FormaErrorBoundary component\n */\ninterface FormaErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Default fallback component shown when an error occurs\n */\nfunction DefaultErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {\n return (\n <div className=\"forma-error-boundary\" role=\"alert\">\n <h3>Something went wrong</h3>\n <p>An error occurred while rendering the form.</p>\n <details>\n <summary>Error details</summary>\n <pre>{error.message}</pre>\n </details>\n <button type=\"button\" onClick={onReset}>\n Try again\n </button>\n </div>\n );\n}\n\n/**\n * Error boundary component for Forma forms\n *\n * Catches JavaScript errors in child component tree and displays\n * a fallback UI instead of crashing the entire application.\n *\n * @example\n * ```tsx\n * <FormaErrorBoundary\n * fallback={<div>Form error occurred</div>}\n * onError={(error) => logError(error)}\n * >\n * <FormRenderer spec={spec} components={components} />\n * </FormaErrorBoundary>\n * ```\n */\nexport class FormaErrorBoundary extends React.Component<\n FormaErrorBoundaryProps,\n FormaErrorBoundaryState\n> {\n constructor(props: FormaErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): FormaErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.props.onError?.(error, errorInfo);\n }\n\n componentDidUpdate(prevProps: FormaErrorBoundaryProps): void {\n // Reset error state when resetKey changes\n if (\n this.state.hasError &&\n prevProps.resetKey !== this.props.resetKey\n ) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): React.ReactNode {\n if (this.state.hasError && this.state.error) {\n const { fallback } = this.props;\n\n if (typeof fallback === \"function\") {\n return fallback(this.state.error, this.reset);\n }\n\n if (fallback) {\n return fallback;\n }\n\n return <DefaultErrorFallback error={this.state.error} onReset={this.reset} />;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";AAOA,SAAS,aAAa,WAAW,SAAS,YAAY,QAAQ,gBAAgB;AAG9E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAyIP,SAAS,YAAY,OAAkB,QAA+B;AACpE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,KAAK,GAAG,OAAO,MAAM;AAAA,QACpD,SAAS;AAAA,QACT,aAAa;AAAA;AAAA,MACf;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,KAAK,GAAG,OAAO,QAAQ;AAAA,MAC9D;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,GAAG;AAAA,QACH,MAAM,EAAE,GAAG,MAAM,MAAM,GAAG,OAAO,OAAO;AAAA,QACxC,SAAS;AAAA,QACT,aAAa;AAAA;AAAA,MACf;AAAA,IACF,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,cAAc,OAAO,aAAa;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,aAAa,OAAO,YAAY;AAAA,IACrD,KAAK;AACH,aAAO,EAAE,GAAG,OAAO,aAAa,OAAO,KAAK;AAAA,IAC9C,KAAK;AACH,aAAO;AAAA,QACL,MAAM,OAAO;AAAA,QACb,SAAS,CAAC;AAAA,QACV,cAAc;AAAA,QACd,aAAa;AAAA,QACb,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,IACF;AACE,aAAO;AAAA,EACX;AACF;AAKO,SAAS,SAAS,SAA0C;AACjE,QAAM,EAAE,MAAM,WAAW,cAAc,CAAC,GAAG,UAAU,UAAU,aAAa,QAAQ,eAAe,uBAAuB,EAAE,IAAI;AAGhI,QAAM,OAAO,QAAQ,MAAa;AAChC,QAAI,CAAC,cAAe,QAAO;AAC3B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,eAAe;AAAA,QACb,GAAG,UAAU;AAAA,QACb,GAAG;AAAA,MACL;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,aAAa,CAAC;AAE7B,QAAM,CAAC,OAAO,QAAQ,IAAI,WAAW,aAAa;AAAA,IAChD,MAAM;AAAA,IACN,SAAS,CAAC;AAAA,IACV,cAAc;AAAA,IACd,aAAa;AAAA,IACb,SAAS;AAAA,IACT,aAAa;AAAA,EACf,CAAC;AAGD,QAAM,iBAAiB,OAAO,KAAK;AAGnC,QAAM,WAAW;AAAA,IACf,MAAM,UAAU,MAAM,MAAM,IAAI;AAAA,IAChC,CAAC,MAAM,MAAM,IAAI;AAAA,EACnB;AAGA,QAAM,aAAa;AAAA,IACjB,MAAM,cAAc,MAAM,MAAM,MAAM,EAAE,SAAS,CAAC;AAAA,IAClD,CAAC,MAAM,MAAM,MAAM,QAAQ;AAAA,EAC7B;AAGA,QAAM,WAAW;AAAA,IACf,MAAM,YAAY,MAAM,MAAM,MAAM,EAAE,SAAS,CAAC;AAAA,IAChD,CAAC,MAAM,MAAM,MAAM,QAAQ;AAAA,EAC7B;AAGA,QAAM,UAAU;AAAA,IACd,MAAM,WAAW,MAAM,MAAM,MAAM,EAAE,SAAS,CAAC;AAAA,IAC/C,CAAC,MAAM,MAAM,MAAM,QAAQ;AAAA,EAC7B;AAGA,QAAM,sBAAsB;AAAA,IAC1B,MAAM,SAAS,MAAM,MAAM,MAAM,EAAE,UAAU,aAAa,KAAK,CAAC;AAAA,IAChE,CAAC,MAAM,MAAM,MAAM,QAAQ;AAAA,EAC7B;AAGA,QAAM,CAAC,qBAAqB,sBAAsB,IAAI,SAA2B,mBAAmB;AAGpG,YAAU,MAAM;AACd,QAAI,wBAAwB,GAAG;AAE7B,6BAAuB,mBAAmB;AAC1C;AAAA,IACF;AAGA,UAAM,YAAY,WAAW,MAAM;AACjC,6BAAuB,mBAAmB;AAAA,IAC5C,GAAG,oBAAoB;AAEvB,WAAO,MAAM,aAAa,SAAS;AAAA,EACrC,GAAG,CAAC,qBAAqB,oBAAoB,CAAC;AAG9C,QAAM,aAAa,uBAAuB,IAAI,sBAAsB;AAKpE,YAAU,MAAM;AACd,QAAI,eAAe,SAAS;AAC1B,2CAAW,MAAM,MAAM;AAAA,IACzB,OAAO;AACL,qBAAe,UAAU;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,UAAU,QAAQ,CAAC;AAGnC,QAAM,iBAAiB,YAAY,CAAC,MAAc,UAAyB;AAEzE,UAAM,QAAQ,KAAK,QAAQ,cAAc,KAAK,EAAE,MAAM,GAAG;AAEzD,QAAI,MAAM,WAAW,GAAG;AAEtB,eAAS,EAAE,MAAM,mBAAmB,OAAO,MAAM,MAAM,CAAC;AACxD;AAAA,IACF;AAGA,UAAM,oBAAoB,CAAC,MAA+B,WAAqB,QAA0C;AACvH,YAAM,SAAS,EAAE,GAAG,KAAK;AACzB,UAAI,UAAmC;AAEvC,eAAS,IAAI,GAAG,IAAI,UAAU,SAAS,GAAG,KAAK;AAC7C,cAAM,OAAO,UAAU,CAAC;AACxB,cAAM,WAAW,UAAU,IAAI,CAAC;AAChC,cAAM,mBAAmB,QAAQ,KAAK,QAAQ;AAE9C,YAAI,QAAQ,IAAI,MAAM,QAAW;AAC/B,kBAAQ,IAAI,IAAI,mBAAmB,CAAC,IAAI,CAAC;AAAA,QAC3C,WAAW,MAAM,QAAQ,QAAQ,IAAI,CAAC,GAAG;AACvC,kBAAQ,IAAI,IAAI,CAAC,GAAI,QAAQ,IAAI,CAAe;AAAA,QAClD,OAAO;AACL,kBAAQ,IAAI,IAAI,EAAE,GAAI,QAAQ,IAAI,EAA8B;AAAA,QAClE;AACA,kBAAU,QAAQ,IAAI;AAAA,MACxB;AAEA,cAAQ,UAAU,UAAU,SAAS,CAAC,CAAC,IAAI;AAC3C,aAAO;AAAA,IACT;AAEA,aAAS,EAAE,MAAM,cAAc,QAAQ,kBAAkB,MAAM,MAAM,OAAO,KAAK,EAAE,CAAC;AAAA,EACtF,GAAG,CAAC,MAAM,IAAI,CAAC;AAGf,QAAM,gBAAgB;AAAA,IACpB,CAAC,MAAc,UAAmB;AAChC,qBAAe,MAAM,KAAK;AAC1B,UAAI,eAAe,UAAU;AAC3B,iBAAS,EAAE,MAAM,qBAAqB,OAAO,MAAM,SAAS,KAAK,CAAC;AAAA,MACpE;AAAA,IACF;AAAA,IACA,CAAC,YAAY,cAAc;AAAA,EAC7B;AAEA,QAAM,kBAAkB,YAAY,CAAC,MAAc,UAAU,SAAS;AACpE,aAAS,EAAE,MAAM,qBAAqB,OAAO,MAAM,QAAQ,CAAC;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,YAAY,CAAC,WAAoC;AACjE,aAAS,EAAE,MAAM,cAAc,OAAO,CAAC;AAAA,EACzC,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB;AAAA,IACpB,CAAC,SAA+B;AAC9B,aAAO,WAAW,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,IAAI;AAAA,IACzD;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,eAAe,YAAY,MAAwB;AACvD,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,CAAC;AAEf,QAAM,aAAa,YAAY,YAAY;AACzC,aAAS,EAAE,MAAM,kBAAkB,cAAc,KAAK,CAAC;AACvD,QAAI;AAEF,UAAI,oBAAoB,SAAS,UAAU;AACzC,cAAM,SAAS,MAAM,IAAI;AAAA,MAC3B;AACA,eAAS,EAAE,MAAM,iBAAiB,aAAa,KAAK,CAAC;AAAA,IACvD,UAAE;AACA,eAAS,EAAE,MAAM,kBAAkB,cAAc,MAAM,CAAC;AAAA,IAC1D;AAAA,EACF,GAAG,CAAC,qBAAqB,UAAU,MAAM,IAAI,CAAC;AAE9C,QAAM,YAAY,YAAY,MAAM;AAClC,aAAS,EAAE,MAAM,SAAS,YAAY,CAAC;AAAA,EACzC,GAAG,CAAC,WAAW,CAAC;AAGhB,QAAM,SAAS,QAAQ,MAA4B;AACjD,QAAI,CAAC,KAAK,SAAS,KAAK,MAAM,WAAW,EAAG,QAAO;AAEnD,UAAM,iBAAiB,kBAAkB,MAAM,MAAM,MAAM,EAAE,SAAS,CAAC;AAGvE,UAAM,QAAqB,KAAK,MAAM,IAAI,CAAC,OAAO;AAAA,MAChD,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,aAAa,EAAE;AAAA,MACf,SAAS,eAAe,EAAE,EAAE,MAAM;AAAA,MAClC,QAAQ,EAAE;AAAA,IACZ,EAAE;AAGF,UAAM,eAAe,MAAM,OAAO,CAAC,MAAM,EAAE,OAAO;AAGlD,UAAM,eAAe,KAAK,IAAI,GAAG,aAAa,SAAS,CAAC;AACxD,UAAM,mBAAmB,KAAK,IAAI,KAAK,IAAI,GAAG,MAAM,WAAW,GAAG,YAAY;AAG9E,QAAI,qBAAqB,MAAM,eAAe,aAAa,SAAS,GAAG;AACrE,eAAS,EAAE,MAAM,YAAY,MAAM,iBAAiB,CAAC;AAAA,IACvD;AAEA,UAAM,cAAc,aAAa,gBAAgB,KAAK;AACtD,UAAM,cAAc,mBAAmB,aAAa,SAAS;AAC7D,UAAM,kBAAkB,mBAAmB;AAC3C,UAAM,aAAa,qBAAqB,aAAa,SAAS;AAE9D,WAAO;AAAA,MACL;AAAA,MACA,kBAAkB;AAAA,MAClB;AAAA,MACA,UAAU,CAAC,UAAkB;AAE3B,cAAM,aAAa,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,GAAG,YAAY;AAC5D,iBAAS,EAAE,MAAM,YAAY,MAAM,WAAW,CAAC;AAAA,MACjD;AAAA,MACA,UAAU,MAAM;AACd,YAAI,aAAa;AACf,mBAAS,EAAE,MAAM,YAAY,MAAM,mBAAmB,EAAE,CAAC;AAAA,QAC3D;AAAA,MACF;AAAA,MACA,cAAc,MAAM;AAClB,YAAI,iBAAiB;AACnB,mBAAS,EAAE,MAAM,YAAY,MAAM,mBAAmB,EAAE,CAAC;AAAA,QAC3D;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa,MAAM;AACjB,YAAI,CAAC,YAAa,QAAO;AAEzB,cAAM,aAAa,WAAW,OAAO,OAAO,CAAC,MAAM;AAEjD,gBAAM,kBAAkB,YAAY,OAAO,SAAS,EAAE,KAAK,KACzD,YAAY,OAAO,KAAK,OAAK,EAAE,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC;AAE1D,gBAAM,YAAY,WAAW,EAAE,KAAK,MAAM;AAE1C,gBAAM,UAAU,EAAE,aAAa;AAC/B,iBAAO,mBAAmB,aAAa;AAAA,QACzC,CAAC;AACD,eAAO,WAAW,WAAW;AAAA,MAC/B,GAAG;AAAA,MACH;AAAA,MACA,wBAAwB,MAAM;AAC5B,YAAI,aAAa;AACf,sBAAY,OAAO,QAAQ,CAAC,UAAU;AACpC,qBAAS,EAAE,MAAM,qBAAqB,OAAO,SAAS,KAAK,CAAC;AAAA,UAC9D,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,qBAAqB,MAAM;AACzB,YAAI,CAAC,YAAa,QAAO;AACzB,cAAM,aAAa,WAAW,OAAO;AAAA,UAAO,CAAC,MAC3C,YAAY,OAAO,SAAS,EAAE,KAAK;AAAA,QACrC;AACA,eAAO,WAAW,WAAW;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,MAAM,MAAM,aAAa,UAAU,YAAY,UAAU,CAAC;AAG1E,QAAM,iBAAiB,YAAY,CAAC,SAA0B;AAE5D,UAAM,QAAQ,KAAK,QAAQ,cAAc,KAAK,EAAE,MAAM,GAAG;AACzD,QAAI,QAAiB,MAAM;AAC3B,eAAW,QAAQ,OAAO;AACxB,UAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,cAAS,MAAkC,IAAI;AAAA,IACjD;AACA,WAAO;AAAA,EACT,GAAG,CAAC,MAAM,IAAI,CAAC;AAGf,QAAM,iBAAiB,YAAY,CAAC,MAAc,UAAyB;AAEzE,UAAM,QAAQ,KAAK,QAAQ,cAAc,KAAK,EAAE,MAAM,GAAG;AACzD,QAAI,MAAM,WAAW,GAAG;AACtB,eAAS,EAAE,MAAM,mBAAmB,OAAO,MAAM,MAAM,CAAC;AACxD;AAAA,IACF;AAGA,UAAM,UAAU,EAAE,GAAG,MAAM,KAAK;AAChC,QAAI,UAAmC;AAEvC,aAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;AACzC,YAAM,OAAO,MAAM,CAAC;AACpB,YAAM,WAAW,MAAM,IAAI,CAAC;AAC5B,YAAM,mBAAmB,QAAQ,KAAK,QAAQ;AAE9C,UAAI,QAAQ,IAAI,MAAM,QAAW;AAC/B,gBAAQ,IAAI,IAAI,mBAAmB,CAAC,IAAI,CAAC;AAAA,MAC3C,WAAW,MAAM,QAAQ,QAAQ,IAAI,CAAC,GAAG;AACvC,gBAAQ,IAAI,IAAI,CAAC,GAAI,QAAQ,IAAI,CAAe;AAAA,MAClD,OAAO;AACL,gBAAQ,IAAI,IAAI,EAAE,GAAI,QAAQ,IAAI,EAA8B;AAAA,MAClE;AACA,gBAAU,QAAQ,IAAI;AAAA,IACxB;AAEA,YAAQ,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;AACnC,aAAS,EAAE,MAAM,cAAc,QAAQ,QAAQ,CAAC;AAAA,EAClD,GAAG,CAAC,MAAM,IAAI,CAAC;AAGf,QAAM,gBAAgB,OAAgF,oBAAI,IAAI,CAAC;AAG/G,YAAU,MAAM;AACd,UAAM,cAAc,IAAI,IAAI,KAAK,UAAU;AAE3C,eAAW,WAAW,KAAK,YAAY;AACrC,YAAM,WAAW,KAAK,OAAO,OAAO;AACpC,UAAI,qCAAU,YAAY;AACxB,mBAAW,OAAO,cAAc,QAAQ,KAAK,GAAG;AAC9C,cAAI,IAAI,WAAW,GAAG,OAAO,GAAG,GAAG;AACjC,wBAAY,IAAI,GAAG;AAAA,UACrB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,eAAW,OAAO,cAAc,QAAQ,KAAK,GAAG;AAC9C,YAAM,YAAY,IAAI,MAAM,GAAG,EAAE,CAAC;AAClC,UAAI,CAAC,YAAY,IAAI,GAAG,KAAK,CAAC,YAAY,IAAI,SAAS,GAAG;AACxD,sBAAc,QAAQ,OAAO,GAAG;AAAA,MAClC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,mBAAmB,YAAY,CAAC,SAAiB;AACrD,QAAI,CAAC,cAAc,QAAQ,IAAI,IAAI,GAAG;AACpC,oBAAc,QAAQ,IAAI,MAAM;AAAA,QAC9B,UAAU,CAAC,UAAmB,eAAe,MAAM,KAAK;AAAA,QACxD,QAAQ,MAAM,gBAAgB,IAAI;AAAA,MACpC,CAAC;AAAA,IACH;AACA,WAAO,cAAc,QAAQ,IAAI,IAAI;AAAA,EACvC,GAAG,CAAC,gBAAgB,eAAe,CAAC;AAGpC,QAAM,gBAAgB,YAAY,CAAC,SAAsC;AACvE,UAAM,WAAW,KAAK,OAAO,IAAI;AACjC,UAAM,WAAW,iBAAiB,IAAI;AAGtC,QAAI,aAAY,qCAAU,SAAQ;AAClC,QAAI,CAAC,aAAa,cAAc,YAAY;AAC1C,YAAM,iBAAiB,KAAK,OAAO,WAAW,IAAI;AAClD,UAAI,gBAAgB;AAClB,YAAI,eAAe,SAAS,SAAU,aAAY;AAAA,iBACzC,eAAe,SAAS,UAAW,aAAY;AAAA,iBAC/C,eAAe,SAAS,UAAW,aAAY;AAAA,iBAC/C,eAAe,SAAS,QAAS,aAAY;AAAA,iBAC7C,eAAe,SAAS,SAAU,aAAY;AAAA,iBAC9C,UAAU,kBAAkB,eAAe,KAAM,aAAY;AAAA,iBAC7D,YAAY,gBAAgB;AACnC,cAAI,eAAe,WAAW,OAAQ,aAAY;AAAA,mBACzC,eAAe,WAAW,YAAa,aAAY;AAAA,mBACnD,eAAe,WAAW,QAAS,aAAY;AAAA,mBAC/C,eAAe,WAAW,MAAO,aAAY;AAAA,QACxD;AAAA,MACF;AAAA,IACF;AAEA,UAAM,cAAc,WAAW,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,IAAI;AACpE,UAAM,YAAY,MAAM,QAAQ,IAAI,KAAK;AACzC,UAAM,aAAa,eAAe,YAAa,eAAe,UAAU,aAAc,MAAM;AAC5F,UAAM,kBAAkB,aAAa,cAAc,CAAC;AACpD,UAAM,YAAY,gBAAgB,SAAS;AAC3C,UAAM,aAAa,SAAS,IAAI,KAAK;AAErC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO,eAAe,IAAI;AAAA,MAC1B,MAAM;AAAA,MACN,QAAO,qCAAU,UAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC;AAAA,MACrE,aAAa,qCAAU;AAAA,MACvB,aAAa,qCAAU;AAAA,MACvB,SAAS,WAAW,IAAI,MAAM;AAAA,MAC9B,SAAS,QAAQ,IAAI,MAAM;AAAA,MAC3B,UAAU;AAAA,MACV,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,UAAU,SAAS;AAAA,MACnB,QAAQ,SAAS;AAAA;AAAA,MAEjB,gBAAgB,aAAa;AAAA,MAC7B,oBAAoB,YAAY,GAAG,IAAI,WAAW;AAAA,MAClD,iBAAiB,cAAc;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,MAAM,MAAM,SAAS,MAAM,aAAa,YAAY,SAAS,UAAU,WAAW,QAAQ,YAAY,gBAAgB,gBAAgB,CAAC;AAG3I,QAAM,sBAAsB,YAAY,CAAC,SAA4C;AACnF,UAAM,YAAY,cAAc,IAAI;AACpC,UAAM,WAAW,KAAK,OAAO,IAAI;AAEjC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,UAAS,qCAAU,YAAW,CAAC;AAAA,IACjC;AAAA,EACF,GAAG,CAAC,eAAe,KAAK,MAAM,CAAC;AAG/B,QAAM,kBAAkB,YAAY,CAAC,SAAwC;AAC3E,UAAM,WAAW,KAAK,OAAO,IAAI;AACjC,UAAM,eAAgB,eAAe,IAAI,KAAmB,CAAC;AAC7D,UAAM,YAAW,qCAAU,aAAY;AACvC,UAAM,YAAW,qCAAU,aAAY;AAEvC,UAAM,SAAS,aAAa,SAAS;AACrC,UAAM,YAAY,aAAa,SAAS;AAExC,UAAM,oBAAoB,CAAC,OAAe,cAA2C;AArmBzF;AAsmBM,YAAM,WAAW,GAAG,IAAI,IAAI,KAAK,KAAK,SAAS;AAC/C,YAAM,gBAAe,0CAAU,eAAV,mBAAuB;AAC5C,YAAM,WAAW,iBAAiB,QAAQ;AAG1C,YAAM,OAAO,aAAa,KAAK;AAC/B,YAAM,YAAY,6BAAO;AAEzB,YAAM,cAAc,WAAW,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ;AACxE,YAAM,YAAY,MAAM,QAAQ,QAAQ,KAAK;AAC7C,YAAM,aAAa,eAAe,YAAa,eAAe,UAAU,aAAc,MAAM;AAE5F,aAAO;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAM,6CAAc,SAAQ;AAAA,QAC5B,QAAO,6CAAc,UAAS,UAAU,OAAO,CAAC,EAAE,YAAY,IAAI,UAAU,MAAM,CAAC;AAAA,QACnF,aAAa,6CAAc;AAAA,QAC3B,aAAa,6CAAc;AAAA,QAC3B,SAAS;AAAA,QACT,SAAS,QAAQ,IAAI,MAAM;AAAA,QAC3B,UAAU;AAAA;AAAA,QACV,SAAS;AAAA,QACT,QAAQ,aAAa,cAAc,CAAC;AAAA,QACpC,UAAU,SAAS;AAAA,QACnB,QAAQ,SAAS;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM,CAAC,SAAkB;AACvB,YAAI,QAAQ;AACV,yBAAe,MAAM,CAAC,GAAG,cAAc,IAAI,CAAC;AAAA,QAC9C;AAAA,MACF;AAAA,MACA,QAAQ,CAAC,UAAkB;AACzB,YAAI,WAAW;AACb,gBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,mBAAS,OAAO,OAAO,CAAC;AACxB,yBAAe,MAAM,QAAQ;AAAA,QAC/B;AAAA,MACF;AAAA,MACA,MAAM,CAAC,MAAc,OAAe;AAClC,cAAM,WAAW,CAAC,GAAG,YAAY;AACjC,cAAM,CAAC,IAAI,IAAI,SAAS,OAAO,MAAM,CAAC;AACtC,iBAAS,OAAO,IAAI,GAAG,IAAI;AAC3B,uBAAe,MAAM,QAAQ;AAAA,MAC/B;AAAA,MACA,MAAM,CAAC,QAAgB,WAAmB;AACxC,cAAM,WAAW,CAAC,GAAG,YAAY;AACjC,SAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC;AAC1E,uBAAe,MAAM,QAAQ;AAAA,MAC/B;AAAA,MACA,QAAQ,CAAC,OAAe,SAAkB;AACxC,YAAI,QAAQ;AACV,gBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,mBAAS,OAAO,OAAO,GAAG,IAAI;AAC9B,yBAAe,MAAM,QAAQ;AAAA,QAC/B;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,KAAK,QAAQ,gBAAgB,gBAAgB,kBAAkB,SAAS,MAAM,SAAS,MAAM,aAAa,WAAW,QAAQ,UAAU,CAAC;AAE5I,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AAAA,IACf,QAAQ,WAAW;AAAA,IACnB,SAAS,WAAW;AAAA,IACpB,cAAc,MAAM;AAAA,IACpB,aAAa,MAAM;AAAA,IACnB,SAAS,MAAM;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AC7rBA,OAAO,SAAS,YAAY,qBAAqB,UAAAA,SAAQ,WAAAC,UAAS,eAAAC,oBAAmB;;;ACHrF,SAAS,eAAe,kBAAkB;AAMnC,IAAM,eAAe,cAAqC,IAAI;AAM9D,SAAS,kBAAkC;AAChD,QAAM,UAAU,WAAW,YAAY;AACvC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,SAAO;AACT;;;ADqCI,SAmZS,UA5YP,KAPF;AAFJ,SAAS,cAAc,EAAE,UAAU,UAAU,aAAa,GAAgB;AACxE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,UAAU,CAAC,MAAM;AACf,UAAE,eAAe;AACjB,iBAAS;AAAA,MACX;AAAA,MAEC;AAAA;AAAA,QACD,oBAAC,YAAO,MAAK,UAAS,UAAU,cAC7B,yBAAe,kBAAkB,UACpC;AAAA;AAAA;AAAA,EACF;AAEJ;AAKA,SAAS,oBAAoB,EAAE,WAAW,OAAO,UAAU,QAAQ,UAAU,QAAQ,GAAsB;AACzG,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,gBAAgB,MAAM,cAAc,GAAG,SAAS,iBAAiB;AACvE,QAAM,YAAY,OAAO,SAAS;AAElC,SACE,qBAAC,SAAI,WAAU,iBAAgB,mBAAiB,WAC7C;AAAA,UAAM,SACL,qBAAC,WAAM,SAAS,WACb;AAAA,YAAM;AAAA,MACN,YAAY,oBAAC,UAAK,WAAU,YAAW,eAAY,QAAO,eAAC;AAAA,MAC3D,YAAY,oBAAC,UAAK,WAAU,WAAU,yBAAW;AAAA,OACpD;AAAA,IAED;AAAA,IACA,aACC;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QACJ,WAAU;AAAA,QACV,MAAK;AAAA,QACL,aAAU;AAAA,QAET,iBAAO,IAAI,CAAC,OAAO,MAClB,oBAAC,UAAa,WAAU,SACrB,gBAAM,WADE,CAEX,CACD;AAAA;AAAA,IACH;AAAA,IAED,MAAM,eACL,oBAAC,OAAE,IAAI,eAAe,WAAU,qBAC7B,gBAAM,aACT;AAAA,KAEJ;AAEJ;AAKA,SAAS,mBAAmB,EAAE,OAAO,aAAa,SAAS,GAAqB;AAC9E,SACE,qBAAC,SAAI,WAAU,gBACb;AAAA,wBAAC,QAAI,iBAAM;AAAA,IACV,eAAe,oBAAC,OAAG,uBAAY;AAAA,IAC/B;AAAA,KACH;AAEJ;AAKA,SAAS,qBAAqB,QAA4E;AACxG,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAW,QAAO,CAAC;AAGnE,QAAM,MAAM,aAAa,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;AACzF,QAAM,MAAM,aAAa,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;AAGzF,MAAI;AACJ,MAAI,gBAAgB,UAAU,OAAO,OAAO,eAAe,UAAU;AACnE,WAAO,OAAO;AAAA,EAChB,WAAW,OAAO,SAAS,WAAW;AACpC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,KAAK,KAAK;AAC1B;AAKA,SAAS,kBAAkB,YAAsE;AAC/F,QAAM,OAAgC,CAAC;AACvC,aAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,QAAI,SAAS,SAAS,WAAW;AAC/B,WAAK,SAAS,IAAI;AAAA,IACpB,WAAW,SAAS,SAAS,YAAY,SAAS,SAAS,WAAW;AACpE,WAAK,SAAS,IAAI;AAAA,IACpB,OAAO;AACL,WAAK,SAAS,IAAI;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAKO,IAAM,eAAe;AAAA,EAC1B,SAASC,cAAa,OAAO,KAAK;AAChC,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,SAAS;AAAA,MACjB,cAAc,eAAe;AAAA,MAC7B,aAAa,cAAc;AAAA,MAC3B;AAAA,IACF,IAAI;AAEJ,UAAM,QAAQ,SAAS;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,UAAM,YAAYC,QAAiC,oBAAI,IAAI,CAAC;AAG5D,UAAM,oBAAoBA,QAMtB,oBAAI,IAAI,CAAC;AAGb,UAAM,aAAaC,aAAY,CAAC,SAAiB;AAC/C,YAAM,UAAU,UAAU,QAAQ,IAAI,IAAI;AAC1C,yCAAS;AAAA,IACX,GAAG,CAAC,CAAC;AAGL,UAAM,kBAAkBA,aAAY,MAAM;AACxC,YAAM,aAAa,MAAM,OAAO,CAAC;AACjC,UAAI,YAAY;AACd,mBAAW,WAAW,KAAK;AAAA,MAC7B;AAAA,IACF,GAAG,CAAC,MAAM,QAAQ,UAAU,CAAC;AAG7B;AAAA,MACE;AAAA,MACA,OAAO;AAAA,QACL,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,cAAc,MAAM;AAAA,QACpB;AAAA,QACA;AAAA,QACA,WAAW,MAAM,MAAM;AAAA,QACvB,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,MACjB;AAAA,MACA,CAAC,OAAO,YAAY,eAAe;AAAA,IACrC;AAGA,UAAM,iBAAiBC,SAAQ,MAAM;AA5OzC;AA6OM,UAAI,KAAK,SAAS,KAAK,MAAM,SAAS,KAAK,MAAM,QAAQ;AAEvD,cAAM,cAAc,MAAM,OAAO;AACjC,YAAI,aAAa;AACf,iBAAO,YAAY;AAAA,QACrB;AAEA,iBAAO,UAAK,MAAM,CAAC,MAAZ,mBAAe,WAAU,CAAC;AAAA,MACnC;AAEA,aAAO,KAAK;AAAA,IACd,GAAG,CAAC,KAAK,OAAO,KAAK,YAAY,MAAM,MAAM,CAAC;AAG9C,UAAM,cAAcD,aAAY,CAAC,cAAsB;AACrD,YAAM,WAAW,KAAK,OAAO,SAAS;AACtC,UAAI,CAAC,SAAU,QAAO;AAEtB,YAAM,YAAY,MAAM,WAAW,SAAS,MAAM;AAClD,UAAI,CAAC,UAAW,QAAO;AAGvB,YAAM,YAAY,SAAS,SAAS,SAAS,aAAa,UAAU;AACpE,YAAM,eAAe;AACrB,YAAM,YAAY,WAAW,YAAY,KAAK,WAAW;AAEzD,UAAI,CAAC,WAAW;AACd,gBAAQ,KAAK,sCAAsC,SAAS,EAAE;AAC9D,eAAO;AAAA,MACT;AAEA,YAAM,SAAS,MAAM,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS;AAC/D,YAAM,UAAU,MAAM,QAAQ,SAAS,KAAK;AAC5C,YAAM,WAAW,MAAM,SAAS,SAAS,KAAK;AAC9C,YAAM,WAAW,MAAM,QAAQ,SAAS,MAAM;AAG9C,YAAM,iBAAiB,KAAK,OAAO,WAAW,SAAS;AAGvD,YAAM,YAA4B;AAAA,QAChC,MAAM;AAAA,QACN,OAAO;AAAA,QACP,OAAO,MAAM,KAAK,SAAS;AAAA,QAC3B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU,CAAC,UAAmB,MAAM,cAAc,WAAW,KAAK;AAAA,QAClE,QAAQ,MAAM,MAAM,gBAAgB,SAAS;AAAA;AAAA,QAE7C,SAAS;AAAA;AAAA,QACT,SAAS,CAAC;AAAA,QACV,OAAO,SAAS,SAAS;AAAA,QACzB,aAAa,SAAS;AAAA,QACtB,aAAa,SAAS;AAAA,MACxB;AAGA,UAAI,aAAsG;AAE1G,UAAI,cAAc,YAAY,cAAc,WAAW;AACrD,cAAM,cAAc,qBAAqB,cAAc;AACvD,qBAAa;AAAA,UACX,GAAG;AAAA,UACH;AAAA,UACA,OAAO,UAAU;AAAA,UACjB,UAAU,UAAU;AAAA,UACpB,GAAG;AAAA,QACL;AAAA,MACF,WAAW,cAAc,YAAY,cAAc,eAAe;AAChE,qBAAa;AAAA,UACX,GAAG;AAAA,UACH;AAAA,UACA,OAAO,UAAU;AAAA,UACjB,UAAU,UAAU;AAAA,UACpB,SAAS,SAAS,WAAW,CAAC;AAAA,QAChC;AAAA,MACF,WAAW,cAAc,WAAW,SAAS,YAAY;AACvD,cAAM,aAAc,UAAU,SAAmC,CAAC;AAClE,cAAM,WAAW,SAAS,YAAY;AACtC,cAAM,WAAW,SAAS,YAAY;AACtC,cAAM,gBAAgB,SAAS;AAI/B,YAAI,CAAC,kBAAkB,QAAQ,IAAI,SAAS,GAAG;AAC7C,4BAAkB,QAAQ,IAAI,WAAW;AAAA,YACvC,MAAM,CAAC,SAAmB;AACxB,oBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,oBAAM,UAAU,QAAQ,kBAAkB,aAAa;AACvD,oBAAM,cAAc,WAAW,CAAC,GAAG,cAAc,OAAO,CAAC;AAAA,YAC3D;AAAA,YACA,QAAQ,CAAC,OAAe,SAAkB;AACxC,oBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,oBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,uBAAS,OAAO,OAAO,GAAG,IAAI;AAC9B,oBAAM,cAAc,WAAW,QAAQ;AAAA,YACzC;AAAA,YACA,QAAQ,CAAC,UAAkB;AACzB,oBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,oBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,uBAAS,OAAO,OAAO,CAAC;AACxB,oBAAM,cAAc,WAAW,QAAQ;AAAA,YACzC;AAAA,YACA,MAAM,CAAC,MAAc,OAAe;AAClC,oBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,oBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,oBAAM,CAAC,IAAI,IAAI,SAAS,OAAO,MAAM,CAAC;AACtC,uBAAS,OAAO,IAAI,GAAG,IAAI;AAC3B,oBAAM,cAAc,WAAW,QAAQ;AAAA,YACzC;AAAA,YACA,MAAM,CAAC,QAAgB,WAAmB;AACxC,oBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,oBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,eAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC;AAC1E,oBAAM,cAAc,WAAW,QAAQ;AAAA,YACzC;AAAA,UACF,CAAC;AAAA,QACH;AACA,cAAM,gBAAgB,kBAAkB,QAAQ,IAAI,SAAS;AAE7D,cAAM,UAAwB;AAAA,UAC5B,OAAO;AAAA,UACP,MAAM,cAAc;AAAA,UACpB,QAAQ,cAAc;AAAA,UACtB,QAAQ,cAAc;AAAA,UACtB,MAAM,cAAc;AAAA,UACpB,MAAM,cAAc;AAAA,UACpB,mBAAmB,CAAC,OAAe,cAAsB;AA9WnE;AA+WY,kBAAM,eAAe,cAAc,SAAS;AAC5C,kBAAM,WAAW,GAAG,SAAS,IAAI,KAAK,KAAK,SAAS;AACpD,kBAAM,aAAa,gBAAW,KAAK,MAAhB,mBAAgD;AACnE,mBAAO;AAAA,cACL,MAAM;AAAA,cACN,OAAO;AAAA,cACP,OAAM,6CAAc,SAAQ;AAAA,cAC5B,QAAO,6CAAc,UAAS;AAAA,cAC9B,aAAa,6CAAc;AAAA,cAC3B,aAAa,6CAAc;AAAA,cAC3B,SAAS;AAAA,cACT,SAAS,CAAC;AAAA,cACV,WAAU,6CAAc,kBAAiB;AAAA,cACzC,SAAS,MAAM,QAAQ,QAAQ,KAAK;AAAA,cACpC,QAAQ,MAAM,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ;AAAA,cACvD,UAAU,CAAC,UAAmB;AAC5B,sBAAM,eAAgB,MAAM,KAAK,SAAS,KAA+B,CAAC;AAC1E,sBAAM,WAAW,CAAC,GAAG,YAAY;AACjC,sBAAM,OAAQ,SAAS,KAAK,KAAK,CAAC;AAClC,yBAAS,KAAK,IAAI,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,MAAM;AAChD,sBAAM,cAAc,WAAW,QAAQ;AAAA,cACzC;AAAA,cACA,QAAQ,MAAM,MAAM,gBAAgB,QAAQ;AAAA,cAC5C,WAAW;AAAA,cACX;AAAA,cACA,SAAS,6CAAc;AAAA,YACzB;AAAA,UACF;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,WAAW,SAAS;AAAA,UAC5B,WAAW,WAAW,SAAS;AAAA,QACjC;AACA,qBAAa;AAAA,UACX,GAAG;AAAA,UACH,WAAW;AAAA,UACX,OAAO;AAAA,UACP,UAAU,UAAU;AAAA,UACpB;AAAA,UACA,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,QACF;AAAA,MACF,OAAO;AAEL,qBAAa;AAAA,UACX,GAAG;AAAA,UACH;AAAA,UACA,OAAQ,UAAU,SAAoB;AAAA,UACtC,UAAU,UAAU;AAAA,QACtB;AAAA,MACF;AAGA,YAAM,iBAAiB,EAAE,OAAO,YAAY,KAAK;AAEjD,aACE;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,OAAO;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UAER,gBAAM,cAAc,WAAyD,cAAc;AAAA;AAAA,QARvF;AAAA,MASP;AAAA,IAEJ,GAAG,CAAC,MAAM,OAAO,YAAY,YAAY,CAAC;AAG1C,UAAM,iBAAiBC;AAAA,MACrB,MAAM,eAAe,IAAI,WAAW;AAAA,MACpC,CAAC,gBAAgB,WAAW;AAAA,IAC9B;AAGA,UAAM,UAAUA,SAAQ,MAAM;AAC5B,UAAI,KAAK,SAAS,KAAK,MAAM,SAAS,KAAK,MAAM,QAAQ;AACvD,cAAM,cAAc,MAAM,OAAO;AACjC,YAAI,CAAC,YAAa,QAAO;AAEzB,eACE;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,YAAY;AAAA,YACnB,aAAa,YAAY;AAAA,YACzB,WAAW,MAAM,OAAO;AAAA,YACxB,YAAY,MAAM,OAAO,MAAM;AAAA,YAE9B;AAAA;AAAA,QACH;AAAA,MAEJ;AAEA,aAAO,gCAAG,0BAAe;AAAA,IAC3B,GAAG,CAAC,KAAK,OAAO,MAAM,QAAQ,aAAa,cAAc,CAAC;AAE1D,WACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,OAC5B;AAAA,MAAC;AAAA;AAAA,QACC,UAAU,MAAM;AAAA,QAChB,cAAc,MAAM;AAAA,QACpB,SAAS,MAAM;AAAA,QAEd;AAAA;AAAA,IACH,GACF;AAAA,EAEJ;AACF;;;AEtdA,OAAOC,YAAW;AA8PP,gBAAAC,YAAA;AAhOX,SAASC,sBAAqB,QAA4E;AACxG,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAW,QAAO,CAAC;AAGnE,QAAM,MAAM,aAAa,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;AACzF,QAAM,MAAM,aAAa,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;AAGzF,MAAI;AACJ,MAAI,gBAAgB,UAAU,OAAO,OAAO,eAAe,UAAU;AACnE,WAAO,OAAO;AAAA,EAChB,WAAW,OAAO,SAAS,WAAW;AACpC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,KAAK,KAAK;AAC1B;AAKA,SAASC,mBAAkB,YAAsE;AAC/F,QAAM,OAAgC,CAAC;AACvC,aAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,QAAI,SAAS,SAAS,WAAW;AAC/B,WAAK,SAAS,IAAI;AAAA,IACpB,WAAW,SAAS,SAAS,YAAY,SAAS,SAAS,WAAW;AACpE,WAAK,SAAS,IAAI;AAAA,IACpB,OAAO;AACL,WAAK,SAAS,IAAI;AAAA,IACpB;AAAA,EACF;AACA,SAAO;AACT;AAWO,SAAS,cAAc,EAAE,WAAW,YAAY,UAAU,GAAuB;AACtF,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,EAAE,KAAK,IAAI;AAEjB,QAAM,WAAW,KAAK,OAAO,SAAS;AACtC,MAAI,CAAC,UAAU;AACb,YAAQ,KAAK,oBAAoB,SAAS,EAAE;AAC5C,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,MAAM,WAAW,SAAS,MAAM;AAClD,MAAI,CAAC,UAAW,QAAO;AAGvB,QAAM,YAAY,SAAS,SAAS,SAAS,aAAa,UAAU;AACpE,QAAM,eAAe;AACrB,QAAM,YAAY,WAAW,YAAY,KAAK,WAAW;AAEzD,MAAI,CAAC,WAAW;AACd,YAAQ,KAAK,sCAAsC,SAAS,EAAE;AAC9D,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,MAAM,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS;AAC/D,QAAM,UAAU,MAAM,QAAQ,SAAS,KAAK;AAC5C,QAAM,WAAW,MAAM,SAAS,SAAS,KAAK;AAC9C,QAAM,WAAW,MAAM,QAAQ,SAAS,MAAM;AAG9C,QAAM,iBAAiB,KAAK,OAAO,WAAW,SAAS;AAGvD,QAAM,YAA4B;AAAA,IAChC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,OAAO,MAAM,KAAK,SAAS;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU,CAAC,UAAmB,MAAM,cAAc,WAAW,KAAK;AAAA,IAClE,QAAQ,MAAM,MAAM,gBAAgB,SAAS;AAAA;AAAA,IAE7C,SAAS;AAAA;AAAA,IACT,SAAS,CAAC;AAAA,IACV,OAAO,SAAS,SAAS;AAAA,IACzB,aAAa,SAAS;AAAA,IACtB,aAAa,SAAS;AAAA,EACxB;AAGA,MAAI,aAAkJ;AAEtJ,MAAI,cAAc,UAAU;AAC1B,UAAM,cAAcD,sBAAqB,cAAc;AACvD,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,WAAW;AAAA,MACX,OAAO,UAAU;AAAA,MACjB,UAAU,UAAU;AAAA,MACpB,GAAG;AAAA,IACL;AAAA,EACF,WAAW,cAAc,WAAW;AAClC,UAAM,cAAcA,sBAAqB,cAAc;AACvD,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,WAAW;AAAA,MACX,OAAO,UAAU;AAAA,MACjB,UAAU,UAAU;AAAA,MACpB,KAAK,YAAY;AAAA,MACjB,KAAK,YAAY;AAAA,IACnB;AAAA,EACF,WAAW,cAAc,UAAU;AACjC,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,WAAW;AAAA,MACX,OAAO,UAAU;AAAA,MACjB,UAAU,UAAU;AAAA,MACpB,SAAS,SAAS,WAAW,CAAC;AAAA,IAChC;AAAA,EACF,WAAW,cAAc,eAAe;AACtC,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,WAAW;AAAA,MACX,OAAQ,UAAU,SAAkC,CAAC;AAAA,MACrD,UAAU,UAAU;AAAA,MACpB,SAAS,SAAS,WAAW,CAAC;AAAA,IAChC;AAAA,EACF,WAAW,cAAc,WAAW,SAAS,YAAY;AACvD,UAAM,aAAc,UAAU,SAAmC,CAAC;AAClE,UAAM,WAAW,SAAS,YAAY;AACtC,UAAM,WAAW,SAAS,YAAY;AACtC,UAAM,gBAAgB,SAAS;AAE/B,UAAM,UAAwB;AAAA,MAC5B,OAAO;AAAA,MACP,MAAM,CAAC,SAAmB;AACxB,cAAM,UAAU,QAAQC,mBAAkB,aAAa;AACvD,cAAM,cAAc,WAAW,CAAC,GAAG,YAAY,OAAO,CAAC;AAAA,MACzD;AAAA,MACA,QAAQ,CAAC,OAAe,SAAkB;AACxC,cAAM,WAAW,CAAC,GAAG,UAAU;AAC/B,iBAAS,OAAO,OAAO,GAAG,IAAI;AAC9B,cAAM,cAAc,WAAW,QAAQ;AAAA,MACzC;AAAA,MACA,QAAQ,CAAC,UAAkB;AACzB,cAAM,WAAW,CAAC,GAAG,UAAU;AAC/B,iBAAS,OAAO,OAAO,CAAC;AACxB,cAAM,cAAc,WAAW,QAAQ;AAAA,MACzC;AAAA,MACA,MAAM,CAAC,MAAc,OAAe;AAClC,cAAM,WAAW,CAAC,GAAG,UAAU;AAC/B,cAAM,CAAC,IAAI,IAAI,SAAS,OAAO,MAAM,CAAC;AACtC,iBAAS,OAAO,IAAI,GAAG,IAAI;AAC3B,cAAM,cAAc,WAAW,QAAQ;AAAA,MACzC;AAAA,MACA,MAAM,CAAC,QAAgB,WAAmB;AACxC,cAAM,WAAW,CAAC,GAAG,UAAU;AAC/B,SAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,SAAS,MAAM,CAAC;AAC1E,cAAM,cAAc,WAAW,QAAQ;AAAA,MACzC;AAAA,MACA,mBAAmB,CAAC,OAAe,cAAsB;AA3M/D;AA4MQ,cAAM,eAAe,cAAc,SAAS;AAC5C,cAAM,WAAW,GAAG,SAAS,IAAI,KAAK,KAAK,SAAS;AACpD,cAAM,aAAa,gBAAW,KAAK,MAAhB,mBAAgD;AACnE,eAAO;AAAA,UACL,MAAM;AAAA,UACN,OAAO;AAAA,UACP,OAAM,6CAAc,SAAQ;AAAA,UAC5B,QAAO,6CAAc,UAAS;AAAA,UAC9B,aAAa,6CAAc;AAAA,UAC3B,aAAa,6CAAc;AAAA,UAC3B,SAAS;AAAA,UACT,SAAS,CAAC;AAAA,UACV,WAAU,6CAAc,kBAAiB;AAAA,UACzC,SAAS,MAAM,QAAQ,QAAQ,KAAK;AAAA,UACpC,QAAQ,MAAM,OAAO,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ;AAAA,UACvD,UAAU,CAAC,UAAmB;AAC5B,kBAAM,WAAW,CAAC,GAAG,UAAU;AAC/B,kBAAM,OAAQ,SAAS,KAAK,KAAK,CAAC;AAClC,qBAAS,KAAK,IAAI,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,MAAM;AAChD,kBAAM,cAAc,WAAW,QAAQ;AAAA,UACzC;AAAA,UACA,QAAQ,MAAM,MAAM,gBAAgB,QAAQ;AAAA,UAC5C,WAAW;AAAA,UACX;AAAA,UACA,SAAS,6CAAc;AAAA,QACzB;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,WAAW,SAAS;AAAA,MAC5B,WAAW,WAAW,SAAS;AAAA,IACjC;AACA,iBAAa;AAAA,MACX,GAAG;AAAA,MACH,WAAW;AAAA,MACX,OAAO;AAAA,MACP,UAAU,UAAU;AAAA,MACpB;AAAA,MACA,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,IACF;AAAA,EACF,OAAO;AAEL,iBAAa;AAAA,MACX,GAAG;AAAA,MACH;AAAA,MACA,OAAQ,UAAU,SAAoB;AAAA,MACtC,UAAU,UAAU;AAAA,IACtB;AAAA,EACF;AAGA,QAAM,iBAAiB,EAAE,OAAO,YAAY,KAAK;AACjD,QAAM,UAAUC,OAAM,cAAc,WAAyD,cAAc;AAE3G,MAAI,WAAW;AACb,WAAO,gBAAAH,KAAC,SAAI,WAAuB,mBAAQ;AAAA,EAC7C;AAEA,SAAO;AACT;;;AClQA,OAAOI,YAAW;AA8BZ,gBAAAC,MAEA,QAAAC,aAFA;AAHN,SAAS,qBAAqB,EAAE,OAAO,QAAQ,GAA0C;AACvF,SACE,gBAAAA,MAAC,SAAI,WAAU,wBAAuB,MAAK,SACzC;AAAA,oBAAAD,KAAC,QAAG,kCAAoB;AAAA,IACxB,gBAAAA,KAAC,OAAE,yDAA2C;AAAA,IAC9C,gBAAAC,MAAC,aACC;AAAA,sBAAAD,KAAC,aAAQ,2BAAa;AAAA,MACtB,gBAAAA,KAAC,SAAK,gBAAM,SAAQ;AAAA,OACtB;AAAA,IACA,gBAAAA,KAAC,YAAO,MAAK,UAAS,SAAS,SAAS,uBAExC;AAAA,KACF;AAEJ;AAkBO,IAAM,qBAAN,cAAiCD,OAAM,UAG5C;AAAA,EACA,YAAY,OAAgC;AAC1C,UAAM,KAAK;AACX,SAAK,QAAQ,EAAE,UAAU,OAAO,OAAO,KAAK;AAAA,EAC9C;AAAA,EAEA,OAAO,yBAAyB,OAAuC;AACrE,WAAO,EAAE,UAAU,MAAM,MAAM;AAAA,EACjC;AAAA,EAEA,kBAAkB,OAAc,WAAkC;AA/EpE;AAgFI,qBAAK,OAAM,YAAX,4BAAqB,OAAO;AAAA,EAC9B;AAAA,EAEA,mBAAmB,WAA0C;AAE3D,QACE,KAAK,MAAM,YACX,UAAU,aAAa,KAAK,MAAM,UAClC;AACA,WAAK,SAAS,EAAE,UAAU,OAAO,OAAO,KAAK,CAAC;AAAA,IAChD;AAAA,EACF;AAAA,EAEA,QAAQ,MAAY;AAClB,SAAK,SAAS,EAAE,UAAU,OAAO,OAAO,KAAK,CAAC;AAAA,EAChD;AAAA,EAEA,SAA0B;AACxB,QAAI,KAAK,MAAM,YAAY,KAAK,MAAM,OAAO;AAC3C,YAAM,EAAE,SAAS,IAAI,KAAK;AAE1B,UAAI,OAAO,aAAa,YAAY;AAClC,eAAO,SAAS,KAAK,MAAM,OAAO,KAAK,KAAK;AAAA,MAC9C;AAEA,UAAI,UAAU;AACZ,eAAO;AAAA,MACT;AAEA,aAAO,gBAAAC,KAAC,wBAAqB,OAAO,KAAK,MAAM,OAAO,SAAS,KAAK,OAAO;AAAA,IAC7E;AAEA,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;","names":["useRef","useMemo","useCallback","FormRenderer","useRef","useCallback","useMemo","React","jsx","getNumberConstraints","createDefaultItem","React","React","jsx","jsxs"]}
|
|
1
|
+
{"version":3,"sources":["../src/useForma.ts","../src/FormRenderer.tsx","../src/context.ts","../src/FieldRenderer.tsx","../src/ErrorBoundary.tsx"],"sourcesContent":["/**\n * useForma Hook\n *\n * Main hook for managing Forma form state.\n * This is a placeholder - the full implementation will be migrated from formidable.\n */\n\nimport { useCallback, useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport type { Forma, FieldError, ValidationResult } from \"@fogpipe/forma-core\";\nimport type { GetFieldPropsResult, GetSelectFieldPropsResult, GetArrayHelpersResult } from \"./types.js\";\nimport {\n getVisibility,\n getRequired,\n getEnabled,\n validate,\n calculate,\n getPageVisibility,\n} from \"@fogpipe/forma-core\";\n\n/**\n * Options for useForma hook\n */\nexport interface UseFormaOptions {\n /** The Forma specification */\n spec: Forma;\n /** Initial form data */\n initialData?: Record<string, unknown>;\n /** Submit handler */\n onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;\n /** Change handler */\n onChange?: (data: Record<string, unknown>, computed?: Record<string, unknown>) => void;\n /** When to validate: on change, blur, or submit only */\n validateOn?: \"change\" | \"blur\" | \"submit\";\n /** Additional reference data to merge with spec.referenceData */\n referenceData?: Record<string, unknown>;\n /**\n * Debounce validation by this many milliseconds.\n * Useful for large forms to improve performance.\n * Set to 0 (default) for immediate validation.\n */\n validationDebounceMs?: number;\n}\n\n/**\n * Form state\n */\ninterface FormState {\n data: Record<string, unknown>;\n touched: Record<string, boolean>;\n isSubmitting: boolean;\n isSubmitted: boolean;\n isDirty: boolean;\n currentPage: number;\n}\n\n/**\n * State actions\n */\ntype FormAction =\n | { type: \"SET_FIELD_VALUE\"; field: string; value: unknown }\n | { type: \"SET_FIELD_TOUCHED\"; field: string; touched: boolean }\n | { type: \"SET_VALUES\"; values: Record<string, unknown> }\n | { type: \"SET_SUBMITTING\"; isSubmitting: boolean }\n | { type: \"SET_SUBMITTED\"; isSubmitted: boolean }\n | { type: \"SET_PAGE\"; page: number }\n | { type: \"RESET\"; initialData: Record<string, unknown> };\n\n/**\n * Page state for multi-page forms\n */\nexport interface PageState {\n id: string;\n title: string;\n description?: string;\n visible: boolean;\n fields: string[];\n}\n\n/**\n * Wizard navigation helpers\n */\nexport interface WizardHelpers {\n pages: PageState[];\n currentPageIndex: number;\n currentPage: PageState | null;\n goToPage: (index: number) => void;\n nextPage: () => void;\n previousPage: () => void;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n canProceed: boolean;\n isLastPage: boolean;\n touchCurrentPageFields: () => void;\n validateCurrentPage: () => boolean;\n}\n\n/**\n * Return type of useForma hook\n */\nexport interface UseFormaReturn {\n /** Current form data */\n data: Record<string, unknown>;\n /** Computed field values */\n computed: Record<string, unknown>;\n /** Field visibility map */\n visibility: Record<string, boolean>;\n /** Field required state map */\n required: Record<string, boolean>;\n /** Field enabled state map */\n enabled: Record<string, boolean>;\n /** Field touched state map */\n touched: Record<string, boolean>;\n /** Validation errors */\n errors: FieldError[];\n /** Whether form is valid */\n isValid: boolean;\n /** Whether form is submitting */\n isSubmitting: boolean;\n /** Whether form has been submitted */\n isSubmitted: boolean;\n /** Whether any field has been modified */\n isDirty: boolean;\n /** The Forma spec */\n spec: Forma;\n /** Wizard helpers (if multi-page) */\n wizard: WizardHelpers | null;\n\n /** Set a field value */\n setFieldValue: (path: string, value: unknown) => void;\n /** Set a field as touched */\n setFieldTouched: (path: string, touched?: boolean) => void;\n /** Set multiple values */\n setValues: (values: Record<string, unknown>) => void;\n /** Validate a single field */\n validateField: (path: string) => FieldError[];\n /** Validate entire form */\n validateForm: () => ValidationResult;\n /** Submit the form */\n submitForm: () => Promise<void>;\n /** Reset the form */\n resetForm: () => void;\n\n // Helper methods for getting field props\n /** Get props for any field */\n getFieldProps: (path: string) => GetFieldPropsResult;\n /** Get props for select field (includes options) */\n getSelectFieldProps: (path: string) => GetSelectFieldPropsResult;\n /** Get array helpers for array field */\n getArrayHelpers: (path: string) => GetArrayHelpersResult;\n}\n\n/**\n * State reducer\n */\nfunction formReducer(state: FormState, action: FormAction): FormState {\n switch (action.type) {\n case \"SET_FIELD_VALUE\":\n return {\n ...state,\n data: { ...state.data, [action.field]: action.value },\n isDirty: true,\n isSubmitted: false, // Clear on data change\n };\n case \"SET_FIELD_TOUCHED\":\n return {\n ...state,\n touched: { ...state.touched, [action.field]: action.touched },\n };\n case \"SET_VALUES\":\n return {\n ...state,\n data: { ...state.data, ...action.values },\n isDirty: true,\n isSubmitted: false, // Clear on data change\n };\n case \"SET_SUBMITTING\":\n return { ...state, isSubmitting: action.isSubmitting };\n case \"SET_SUBMITTED\":\n return { ...state, isSubmitted: action.isSubmitted };\n case \"SET_PAGE\":\n return { ...state, currentPage: action.page };\n case \"RESET\":\n return {\n data: action.initialData,\n touched: {},\n isSubmitting: false,\n isSubmitted: false,\n isDirty: false,\n currentPage: 0,\n };\n default:\n return state;\n }\n}\n\n/**\n * Get default initial values for boolean fields.\n * Boolean fields default to false to avoid undefined state,\n * which provides better UX since false is a valid answer.\n */\nfunction getDefaultBooleanValues(spec: Forma): Record<string, boolean> {\n const defaults: Record<string, boolean> = {};\n for (const fieldPath of spec.fieldOrder) {\n const schemaProperty = spec.schema.properties?.[fieldPath];\n const fieldDef = spec.fields[fieldPath];\n if (schemaProperty?.type === \"boolean\" || fieldDef?.type === \"boolean\") {\n defaults[fieldPath] = false;\n }\n }\n return defaults;\n}\n\n/**\n * Main Forma hook\n */\nexport function useForma(options: UseFormaOptions): UseFormaReturn {\n const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = \"blur\", referenceData, validationDebounceMs = 0 } = options;\n\n // Merge referenceData from options with spec.referenceData\n const spec = useMemo((): Forma => {\n if (!referenceData) return inputSpec;\n return {\n ...inputSpec,\n referenceData: {\n ...inputSpec.referenceData,\n ...referenceData,\n },\n };\n }, [inputSpec, referenceData]);\n\n const [state, dispatch] = useReducer(formReducer, {\n data: { ...getDefaultBooleanValues(spec), ...initialData }, // Boolean defaults merged UNDER initialData\n touched: {},\n isSubmitting: false,\n isSubmitted: false,\n isDirty: false,\n currentPage: 0,\n });\n\n // Track if we've initialized (to avoid calling onChange on first render)\n const hasInitialized = useRef(false);\n\n // Calculate computed values\n const computed = useMemo(\n () => calculate(state.data, spec),\n [state.data, spec]\n );\n\n // Calculate visibility\n const visibility = useMemo(\n () => getVisibility(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Calculate required state\n const required = useMemo(\n () => getRequired(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Calculate enabled state\n const enabled = useMemo(\n () => getEnabled(state.data, spec, { computed }),\n [state.data, spec, computed]\n );\n\n // Validate form - compute immediate result\n const immediateValidation = useMemo(\n () => validate(state.data, spec, { computed, onlyVisible: true }),\n [state.data, spec, computed]\n );\n\n // Debounced validation state (only used when validationDebounceMs > 0)\n const [debouncedValidation, setDebouncedValidation] = useState<ValidationResult>(immediateValidation);\n\n // Apply debouncing if configured\n useEffect(() => {\n if (validationDebounceMs <= 0) {\n // No debouncing - use immediate validation\n setDebouncedValidation(immediateValidation);\n return;\n }\n\n // Debounce validation updates\n const timeoutId = setTimeout(() => {\n setDebouncedValidation(immediateValidation);\n }, validationDebounceMs);\n\n return () => clearTimeout(timeoutId);\n }, [immediateValidation, validationDebounceMs]);\n\n // Use debounced validation for display, but immediate for submit\n const validation = validationDebounceMs > 0 ? debouncedValidation : immediateValidation;\n\n // isDirty is tracked via reducer state for O(1) performance\n\n // Call onChange when data changes (not on initial render)\n useEffect(() => {\n if (hasInitialized.current) {\n onChange?.(state.data, computed);\n } else {\n hasInitialized.current = true;\n }\n }, [state.data, computed, onChange]);\n\n // Helper function to set value at nested path\n const setNestedValue = useCallback((path: string, value: unknown): void => {\n // Handle array index notation: \"items[0].name\" -> nested structure\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n\n if (parts.length === 1) {\n // Simple path - just set directly\n dispatch({ type: \"SET_FIELD_VALUE\", field: path, value });\n return;\n }\n\n // Build nested object for complex paths\n const buildNestedObject = (data: Record<string, unknown>, pathParts: string[], val: unknown): Record<string, unknown> => {\n const result = { ...data };\n let current: Record<string, unknown> = result;\n\n for (let i = 0; i < pathParts.length - 1; i++) {\n const part = pathParts[i];\n const nextPart = pathParts[i + 1];\n const isNextArrayIndex = /^\\d+$/.test(nextPart);\n\n if (current[part] === undefined) {\n current[part] = isNextArrayIndex ? [] : {};\n } else if (Array.isArray(current[part])) {\n current[part] = [...(current[part] as unknown[])];\n } else {\n current[part] = { ...(current[part] as Record<string, unknown>) };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[pathParts[pathParts.length - 1]] = val;\n return result;\n };\n\n dispatch({ type: \"SET_VALUES\", values: buildNestedObject(state.data, parts, value) });\n }, [state.data]);\n\n // Actions\n const setFieldValue = useCallback(\n (path: string, value: unknown) => {\n setNestedValue(path, value);\n if (validateOn === \"change\") {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field: path, touched: true });\n }\n },\n [validateOn, setNestedValue]\n );\n\n const setFieldTouched = useCallback((path: string, touched = true) => {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field: path, touched });\n }, []);\n\n const setValues = useCallback((values: Record<string, unknown>) => {\n dispatch({ type: \"SET_VALUES\", values });\n }, []);\n\n const validateField = useCallback(\n (path: string): FieldError[] => {\n return validation.errors.filter((e) => e.field === path);\n },\n [validation]\n );\n\n const validateForm = useCallback((): ValidationResult => {\n return validation;\n }, [validation]);\n\n const submitForm = useCallback(async () => {\n dispatch({ type: \"SET_SUBMITTING\", isSubmitting: true });\n try {\n // Always use immediate validation on submit to ensure accurate result\n if (immediateValidation.valid && onSubmit) {\n await onSubmit(state.data);\n }\n dispatch({ type: \"SET_SUBMITTED\", isSubmitted: true });\n } finally {\n dispatch({ type: \"SET_SUBMITTING\", isSubmitting: false });\n }\n }, [immediateValidation, onSubmit, state.data]);\n\n const resetForm = useCallback(() => {\n dispatch({ type: \"RESET\", initialData });\n }, [initialData]);\n\n // Wizard helpers\n const wizard = useMemo((): WizardHelpers | null => {\n if (!spec.pages || spec.pages.length === 0) return null;\n\n const pageVisibility = getPageVisibility(state.data, spec, { computed });\n\n // Include all pages with their visibility status\n const pages: PageState[] = spec.pages.map((p) => ({\n id: p.id,\n title: p.title,\n description: p.description,\n visible: pageVisibility[p.id] !== false,\n fields: p.fields,\n }));\n\n // For navigation, only count visible pages\n const visiblePages = pages.filter((p) => p.visible);\n\n // Clamp currentPage to valid range (handles case where current page becomes hidden)\n const maxPageIndex = Math.max(0, visiblePages.length - 1);\n const clampedPageIndex = Math.min(Math.max(0, state.currentPage), maxPageIndex);\n\n // Auto-correct page index if it's out of bounds\n if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex });\n }\n\n const currentPage = visiblePages[clampedPageIndex] || null;\n const hasNextPage = clampedPageIndex < visiblePages.length - 1;\n const hasPreviousPage = clampedPageIndex > 0;\n const isLastPage = clampedPageIndex === visiblePages.length - 1;\n\n return {\n pages,\n currentPageIndex: clampedPageIndex,\n currentPage,\n goToPage: (index: number) => {\n // Clamp to valid range\n const validIndex = Math.min(Math.max(0, index), maxPageIndex);\n dispatch({ type: \"SET_PAGE\", page: validIndex });\n },\n nextPage: () => {\n if (hasNextPage) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex + 1 });\n }\n },\n previousPage: () => {\n if (hasPreviousPage) {\n dispatch({ type: \"SET_PAGE\", page: clampedPageIndex - 1 });\n }\n },\n hasNextPage,\n hasPreviousPage,\n canProceed: (() => {\n if (!currentPage) return true;\n // Get errors only for visible fields on the current page\n const pageErrors = validation.errors.filter((e) => {\n // Check if field is on current page (including array items like \"items[0].name\")\n const isOnCurrentPage = currentPage.fields.includes(e.field) ||\n currentPage.fields.some(f => e.field.startsWith(`${f}[`));\n // Only count errors for visible fields\n const isVisible = visibility[e.field] !== false;\n // Only count actual errors, not warnings\n const isError = e.severity === 'error';\n return isOnCurrentPage && isVisible && isError;\n });\n return pageErrors.length === 0;\n })(),\n isLastPage,\n touchCurrentPageFields: () => {\n if (currentPage) {\n currentPage.fields.forEach((field) => {\n dispatch({ type: \"SET_FIELD_TOUCHED\", field, touched: true });\n });\n }\n },\n validateCurrentPage: () => {\n if (!currentPage) return true;\n const pageErrors = validation.errors.filter((e) =>\n currentPage.fields.includes(e.field)\n );\n return pageErrors.length === 0;\n },\n };\n }, [spec, state.data, state.currentPage, computed, validation, visibility]);\n\n // Helper to get value at nested path\n const getValueAtPath = useCallback((path: string): unknown => {\n // Handle array index notation: \"items[0].name\" -> [\"items\", \"0\", \"name\"]\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n let value: unknown = state.data;\n for (const part of parts) {\n if (value === null || value === undefined) return undefined;\n value = (value as Record<string, unknown>)[part];\n }\n return value;\n }, [state.data]);\n\n // Helper to set value at nested path\n const setValueAtPath = useCallback((path: string, value: unknown): void => {\n // For nested paths, we need to build the nested structure\n const parts = path.replace(/\\[(\\d+)\\]/g, '.$1').split('.');\n if (parts.length === 1) {\n dispatch({ type: \"SET_FIELD_VALUE\", field: path, value });\n return;\n }\n\n // Build nested object\n const newData = { ...state.data };\n let current: Record<string, unknown> = newData;\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n const nextPart = parts[i + 1];\n const isNextArrayIndex = /^\\d+$/.test(nextPart);\n\n if (current[part] === undefined) {\n current[part] = isNextArrayIndex ? [] : {};\n } else if (Array.isArray(current[part])) {\n current[part] = [...(current[part] as unknown[])];\n } else {\n current[part] = { ...(current[part] as Record<string, unknown>) };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n current[parts[parts.length - 1]] = value;\n dispatch({ type: \"SET_VALUES\", values: newData });\n }, [state.data]);\n\n // Memoized onChange/onBlur handlers for fields\n const fieldHandlers = useRef<Map<string, { onChange: (value: unknown) => void; onBlur: () => void }>>(new Map());\n\n // Clean up stale field handlers when spec changes to prevent memory leaks\n useEffect(() => {\n const validFields = new Set(spec.fieldOrder);\n // Also include array item field patterns\n for (const fieldId of spec.fieldOrder) {\n const fieldDef = spec.fields[fieldId];\n if (fieldDef?.itemFields) {\n for (const key of fieldHandlers.current.keys()) {\n if (key.startsWith(`${fieldId}[`)) {\n validFields.add(key);\n }\n }\n }\n }\n // Remove handlers for fields that no longer exist\n for (const key of fieldHandlers.current.keys()) {\n const baseField = key.split('[')[0];\n if (!validFields.has(key) && !validFields.has(baseField)) {\n fieldHandlers.current.delete(key);\n }\n }\n }, [spec]);\n\n const getFieldHandlers = useCallback((path: string) => {\n if (!fieldHandlers.current.has(path)) {\n fieldHandlers.current.set(path, {\n onChange: (value: unknown) => setValueAtPath(path, value),\n onBlur: () => setFieldTouched(path),\n });\n }\n return fieldHandlers.current.get(path)!;\n }, [setValueAtPath, setFieldTouched]);\n\n // Get field props for any field\n const getFieldProps = useCallback((path: string): GetFieldPropsResult => {\n const fieldDef = spec.fields[path];\n const handlers = getFieldHandlers(path);\n\n // Determine field type from definition or infer from schema\n let fieldType = fieldDef?.type || \"text\";\n if (!fieldType || fieldType === \"computed\") {\n const schemaProperty = spec.schema.properties[path];\n if (schemaProperty) {\n if (schemaProperty.type === \"number\") fieldType = \"number\";\n else if (schemaProperty.type === \"integer\") fieldType = \"integer\";\n else if (schemaProperty.type === \"boolean\") fieldType = \"boolean\";\n else if (schemaProperty.type === \"array\") fieldType = \"array\";\n else if (schemaProperty.type === \"object\") fieldType = \"object\";\n else if (\"enum\" in schemaProperty && schemaProperty.enum) fieldType = \"select\";\n else if (\"format\" in schemaProperty) {\n if (schemaProperty.format === \"date\") fieldType = \"date\";\n else if (schemaProperty.format === \"date-time\") fieldType = \"datetime\";\n else if (schemaProperty.format === \"email\") fieldType = \"email\";\n else if (schemaProperty.format === \"uri\") fieldType = \"url\";\n }\n }\n }\n\n const fieldErrors = validation.errors.filter((e) => e.field === path);\n const isTouched = state.touched[path] ?? false;\n const showErrors = validateOn === \"change\" || (validateOn === \"blur\" && isTouched) || state.isSubmitted;\n const displayedErrors = showErrors ? fieldErrors : [];\n const hasErrors = displayedErrors.length > 0;\n const isRequired = required[path] ?? false;\n\n // Boolean fields: hide asterisk unless they have validation rules (consent pattern)\n // - Binary question (\"Do you smoke?\"): no validation → false is valid → hide asterisk\n // - Consent checkbox (\"I accept terms\"): has validation rule → show asterisk\n const schemaProperty = spec.schema.properties[path];\n const isBooleanField = schemaProperty?.type === \"boolean\" || fieldDef?.type === \"boolean\";\n const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;\n const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);\n\n return {\n name: path,\n value: getValueAtPath(path),\n type: fieldType,\n label: fieldDef?.label || path.charAt(0).toUpperCase() + path.slice(1),\n description: fieldDef?.description,\n placeholder: fieldDef?.placeholder,\n visible: visibility[path] !== false,\n enabled: enabled[path] !== false,\n required: isRequired,\n showRequiredIndicator,\n touched: isTouched,\n errors: displayedErrors,\n onChange: handlers.onChange,\n onBlur: handlers.onBlur,\n // ARIA accessibility attributes\n \"aria-invalid\": hasErrors || undefined,\n \"aria-describedby\": hasErrors ? `${path}-error` : undefined,\n \"aria-required\": isRequired || undefined,\n };\n }, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);\n\n // Get select field props\n const getSelectFieldProps = useCallback((path: string): GetSelectFieldPropsResult => {\n const baseProps = getFieldProps(path);\n const fieldDef = spec.fields[path];\n\n return {\n ...baseProps,\n options: fieldDef?.options ?? [],\n };\n }, [getFieldProps, spec.fields]);\n\n // Get array helpers\n const getArrayHelpers = useCallback((path: string): GetArrayHelpersResult => {\n const fieldDef = spec.fields[path];\n const currentValue = (getValueAtPath(path) as unknown[]) ?? [];\n const minItems = fieldDef?.minItems ?? 0;\n const maxItems = fieldDef?.maxItems ?? Infinity;\n\n const canAdd = currentValue.length < maxItems;\n const canRemove = currentValue.length > minItems;\n\n const getItemFieldProps = (index: number, fieldName: string): GetFieldPropsResult => {\n const itemPath = `${path}[${index}].${fieldName}`;\n const itemFieldDef = fieldDef?.itemFields?.[fieldName];\n const handlers = getFieldHandlers(itemPath);\n\n // Get item value\n const item = currentValue[index] as Record<string, unknown> | undefined;\n const itemValue = item?.[fieldName];\n\n const fieldErrors = validation.errors.filter((e) => e.field === itemPath);\n const isTouched = state.touched[itemPath] ?? false;\n const showErrors = validateOn === \"change\" || (validateOn === \"blur\" && isTouched) || state.isSubmitted;\n\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type || \"text\",\n label: itemFieldDef?.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: enabled[path] !== false,\n required: false, // TODO: Evaluate item field required\n showRequiredIndicator: false, // Item fields don't show required indicator\n touched: isTouched,\n errors: showErrors ? fieldErrors : [],\n onChange: handlers.onChange,\n onBlur: handlers.onBlur,\n };\n };\n\n return {\n items: currentValue,\n push: (item: unknown) => {\n if (canAdd) {\n setValueAtPath(path, [...currentValue, item]);\n }\n },\n remove: (index: number) => {\n if (canRemove) {\n const newArray = [...currentValue];\n newArray.splice(index, 1);\n setValueAtPath(path, newArray);\n }\n },\n move: (from: number, to: number) => {\n const newArray = [...currentValue];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n setValueAtPath(path, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const newArray = [...currentValue];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n setValueAtPath(path, newArray);\n },\n insert: (index: number, item: unknown) => {\n if (canAdd) {\n const newArray = [...currentValue];\n newArray.splice(index, 0, item);\n setValueAtPath(path, newArray);\n }\n },\n getItemFieldProps,\n minItems,\n maxItems,\n canAdd,\n canRemove,\n };\n }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn]);\n\n return {\n data: state.data,\n computed,\n visibility,\n required,\n enabled,\n touched: state.touched,\n errors: validation.errors,\n isValid: validation.valid,\n isSubmitting: state.isSubmitting,\n isSubmitted: state.isSubmitted,\n isDirty: state.isDirty,\n spec,\n wizard,\n setFieldValue,\n setFieldTouched,\n setValues,\n validateField,\n validateForm,\n submitForm,\n resetForm,\n getFieldProps,\n getSelectFieldProps,\n getArrayHelpers,\n };\n}\n","/**\n * FormRenderer Component\n *\n * Renders a complete form from a Forma specification.\n * Supports single-page and multi-page (wizard) forms.\n */\n\nimport React, { forwardRef, useImperativeHandle, useRef, useMemo, useCallback } from \"react\";\nimport type { Forma, FieldDefinition, ValidationResult, JSONSchemaProperty } from \"@fogpipe/forma-core\";\nimport { useForma } from \"./useForma.js\";\nimport { FormaContext } from \"./context.js\";\nimport type { ComponentMap, LayoutProps, FieldWrapperProps, PageWrapperProps, BaseFieldProps, TextFieldProps, NumberFieldProps, SelectFieldProps, ArrayFieldProps, ArrayHelpers } from \"./types.js\";\n\n/**\n * Props for FormRenderer component\n */\nexport interface FormRendererProps {\n /** The Forma specification */\n spec: Forma;\n /** Initial form data */\n initialData?: Record<string, unknown>;\n /** Submit handler */\n onSubmit?: (data: Record<string, unknown>) => void | Promise<void>;\n /** Change handler */\n onChange?: (data: Record<string, unknown>, computed?: Record<string, unknown>) => void;\n /** Component map for rendering fields */\n components: ComponentMap;\n /** Custom layout component */\n layout?: React.ComponentType<LayoutProps>;\n /** Custom field wrapper component */\n fieldWrapper?: React.ComponentType<FieldWrapperProps>;\n /** Custom page wrapper component */\n pageWrapper?: React.ComponentType<PageWrapperProps>;\n /** When to validate */\n validateOn?: \"change\" | \"blur\" | \"submit\";\n /** Current page for controlled wizard */\n page?: number;\n}\n\n/**\n * Imperative handle for FormRenderer\n */\nexport interface FormRendererHandle {\n submitForm: () => Promise<void>;\n resetForm: () => void;\n validateForm: () => ValidationResult;\n focusField: (path: string) => void;\n focusFirstError: () => void;\n getValues: () => Record<string, unknown>;\n setValues: (values: Record<string, unknown>) => void;\n isValid: boolean;\n isDirty: boolean;\n}\n\n/**\n * Default layout component\n */\nfunction DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n onSubmit();\n }}\n >\n {children}\n <button type=\"submit\" disabled={isSubmitting}>\n {isSubmitting ? \"Submitting...\" : \"Submit\"}\n </button>\n </form>\n );\n}\n\n/**\n * Default field wrapper component with accessibility support\n */\nfunction DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }: FieldWrapperProps) {\n if (!visible) return null;\n\n const errorId = `${fieldPath}-error`;\n const descriptionId = field.description ? `${fieldPath}-description` : undefined;\n const hasErrors = errors.length > 0;\n\n return (\n <div className=\"field-wrapper\" data-field-path={fieldPath}>\n {field.label && (\n <label htmlFor={fieldPath}>\n {field.label}\n {showRequiredIndicator && <span className=\"required\" aria-hidden=\"true\">*</span>}\n {showRequiredIndicator && <span className=\"sr-only\"> (required)</span>}\n </label>\n )}\n {children}\n {hasErrors && (\n <div\n id={errorId}\n className=\"field-errors\"\n role=\"alert\"\n aria-live=\"polite\"\n >\n {errors.map((error, i) => (\n <span key={i} className=\"error\">\n {error.message}\n </span>\n ))}\n </div>\n )}\n {field.description && (\n <p id={descriptionId} className=\"field-description\">\n {field.description}\n </p>\n )}\n </div>\n );\n}\n\n/**\n * Default page wrapper component\n */\nfunction DefaultPageWrapper({ title, description, children }: PageWrapperProps) {\n return (\n <div className=\"page-wrapper\">\n <h2>{title}</h2>\n {description && <p>{description}</p>}\n {children}\n </div>\n );\n}\n\n/**\n * Extract numeric constraints from JSON Schema property\n */\nfunction getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?: number; step?: number } {\n if (!schema) return {};\n if (schema.type !== \"number\" && schema.type !== \"integer\") return {};\n\n // Extract min/max from schema\n const min = \"minimum\" in schema && typeof schema.minimum === \"number\" ? schema.minimum : undefined;\n const max = \"maximum\" in schema && typeof schema.maximum === \"number\" ? schema.maximum : undefined;\n\n // Use multipleOf for step if defined, otherwise default to 1 for integers\n let step: number | undefined;\n if (\"multipleOf\" in schema && typeof schema.multipleOf === \"number\") {\n step = schema.multipleOf;\n } else if (schema.type === \"integer\") {\n step = 1;\n }\n\n return { min, max, step };\n}\n\n/**\n * Create a default item for an array field based on item field definitions\n */\nfunction createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {\n const item: Record<string, unknown> = {};\n for (const [fieldName, fieldDef] of Object.entries(itemFields)) {\n if (fieldDef.type === \"boolean\") {\n item[fieldName] = false;\n } else if (fieldDef.type === \"number\" || fieldDef.type === \"integer\") {\n item[fieldName] = null;\n } else {\n item[fieldName] = \"\";\n }\n }\n return item;\n}\n\n/**\n * FormRenderer component\n */\nexport const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(\n function FormRenderer(props, ref) {\n const {\n spec,\n initialData,\n onSubmit,\n onChange,\n components,\n layout: Layout = DefaultLayout,\n fieldWrapper: FieldWrapper = DefaultFieldWrapper,\n pageWrapper: PageWrapper = DefaultPageWrapper,\n validateOn,\n } = props;\n\n const forma = useForma({\n spec,\n initialData,\n onSubmit,\n onChange,\n validateOn,\n });\n\n const fieldRefs = useRef<Map<string, HTMLElement>>(new Map());\n\n // Cache for array helper functions to prevent recreation on every render\n const arrayHelpersCache = useRef<Map<string, {\n push: (item?: unknown) => void;\n insert: (index: number, item: unknown) => void;\n remove: (index: number) => void;\n move: (from: number, to: number) => void;\n swap: (indexA: number, indexB: number) => void;\n }>>(new Map());\n\n // Focus a specific field by path\n const focusField = useCallback((path: string) => {\n const element = fieldRefs.current.get(path);\n element?.focus();\n }, []);\n\n // Focus the first field with an error\n const focusFirstError = useCallback(() => {\n const firstError = forma.errors[0];\n if (firstError) {\n focusField(firstError.field);\n }\n }, [forma.errors, focusField]);\n\n // Expose imperative handle\n useImperativeHandle(\n ref,\n () => ({\n submitForm: forma.submitForm,\n resetForm: forma.resetForm,\n validateForm: forma.validateForm,\n focusField,\n focusFirstError,\n getValues: () => forma.data,\n setValues: forma.setValues,\n isValid: forma.isValid,\n isDirty: forma.isDirty,\n }),\n [forma, focusField, focusFirstError]\n );\n\n // Determine which fields to render based on pages or fieldOrder\n const fieldsToRender = useMemo(() => {\n if (spec.pages && spec.pages.length > 0 && forma.wizard) {\n // Wizard mode - render fields for the active page\n const currentPage = forma.wizard.currentPage;\n if (currentPage) {\n return currentPage.fields;\n }\n // Fallback to first page\n return spec.pages[0]?.fields ?? [];\n }\n // Single page mode - render all fields in order\n return spec.fieldOrder;\n }, [spec.pages, spec.fieldOrder, forma.wizard]);\n\n // Render a single field (memoized)\n const renderField = useCallback((fieldPath: string) => {\n const fieldDef = spec.fields[fieldPath];\n if (!fieldDef) return null;\n\n const isVisible = forma.visibility[fieldPath] !== false;\n if (!isVisible) return null;\n\n // Infer field type\n const fieldType = fieldDef.type || (fieldDef.itemFields ? \"array\" : \"text\");\n const componentKey = fieldType as keyof ComponentMap;\n const Component = components[componentKey] || components.fallback;\n\n if (!Component) {\n console.warn(`No component found for field type: ${fieldType}`);\n return null;\n }\n\n const errors = forma.errors.filter((e) => e.field === fieldPath);\n const touched = forma.touched[fieldPath] ?? false;\n const required = forma.required[fieldPath] ?? false;\n const disabled = forma.enabled[fieldPath] === false;\n\n // Get schema property for additional constraints\n const schemaProperty = spec.schema.properties[fieldPath];\n\n // Boolean fields: hide asterisk unless they have validation rules (consent pattern)\n // - Binary question (\"Do you smoke?\"): no validation → false is valid → hide asterisk\n // - Consent checkbox (\"I accept terms\"): has validation rule → show asterisk\n const isBooleanField = schemaProperty?.type === \"boolean\" || fieldDef?.type === \"boolean\";\n const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;\n const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);\n\n // Base field props\n const baseProps: BaseFieldProps = {\n name: fieldPath,\n field: fieldDef,\n value: forma.data[fieldPath],\n touched,\n required,\n disabled,\n errors,\n onChange: (value: unknown) => forma.setFieldValue(fieldPath, value),\n onBlur: () => forma.setFieldTouched(fieldPath),\n // Convenience properties\n visible: true, // Always true since we already filtered for visibility\n enabled: !disabled,\n label: fieldDef.label ?? fieldPath,\n description: fieldDef.description,\n placeholder: fieldDef.placeholder,\n };\n\n // Build type-specific props\n let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | SelectFieldProps | ArrayFieldProps = baseProps;\n\n if (fieldType === \"number\" || fieldType === \"integer\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType,\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n ...constraints,\n } as NumberFieldProps;\n } else if (fieldType === \"select\" || fieldType === \"multiselect\") {\n fieldProps = {\n ...baseProps,\n fieldType,\n value: baseProps.value as string | string[] | null,\n onChange: baseProps.onChange as (value: string | string[] | null) => void,\n options: fieldDef.options ?? [],\n } as SelectFieldProps;\n } else if (fieldType === \"array\" && fieldDef.itemFields) {\n const arrayValue = (baseProps.value as unknown[] | undefined) ?? [];\n const minItems = fieldDef.minItems ?? 0;\n const maxItems = fieldDef.maxItems ?? Infinity;\n const itemFieldDefs = fieldDef.itemFields;\n\n // Get or create cached helper functions for this array field\n // These functions read current values when called, not when created\n if (!arrayHelpersCache.current.has(fieldPath)) {\n arrayHelpersCache.current.set(fieldPath, {\n push: (item?: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newItem = item ?? createDefaultItem(itemFieldDefs);\n forma.setFieldValue(fieldPath, [...currentArray, newItem]);\n },\n insert: (index: number, item: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n newArray.splice(index, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n remove: (index: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n newArray.splice(index, 1);\n forma.setFieldValue(fieldPath, newArray);\n },\n move: (from: number, to: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n forma.setFieldValue(fieldPath, newArray);\n },\n });\n }\n const cachedHelpers = arrayHelpersCache.current.get(fieldPath)!;\n\n const helpers: ArrayHelpers = {\n items: arrayValue,\n push: cachedHelpers.push,\n insert: cachedHelpers.insert,\n remove: cachedHelpers.remove,\n move: cachedHelpers.move,\n swap: cachedHelpers.swap,\n getItemFieldProps: (index: number, fieldName: string) => {\n const itemFieldDef = itemFieldDefs[fieldName];\n const itemPath = `${fieldPath}[${index}].${fieldName}`;\n const itemValue = (arrayValue[index] as Record<string, unknown>)?.[fieldName];\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type ?? \"text\",\n label: itemFieldDef?.label ?? fieldName,\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: !disabled,\n required: itemFieldDef?.requiredWhen === \"true\",\n touched: forma.touched[itemPath] ?? false,\n errors: forma.errors.filter((e) => e.field === itemPath),\n onChange: (value: unknown) => {\n const currentArray = (forma.data[fieldPath] as unknown[] | undefined) ?? [];\n const newArray = [...currentArray];\n const item = (newArray[index] ?? {}) as Record<string, unknown>;\n newArray[index] = { ...item, [fieldName]: value };\n forma.setFieldValue(fieldPath, newArray);\n },\n onBlur: () => forma.setFieldTouched(itemPath),\n itemIndex: index,\n fieldName,\n options: itemFieldDef?.options,\n };\n },\n minItems,\n maxItems,\n canAdd: arrayValue.length < maxItems,\n canRemove: arrayValue.length > minItems,\n };\n fieldProps = {\n ...baseProps,\n fieldType: \"array\",\n value: arrayValue,\n onChange: baseProps.onChange as (value: unknown[]) => void,\n helpers,\n itemFields: itemFieldDefs,\n minItems,\n maxItems,\n } as ArrayFieldProps;\n } else {\n // Text-based fields\n fieldProps = {\n ...baseProps,\n fieldType: fieldType as \"text\" | \"email\" | \"password\" | \"url\" | \"textarea\",\n value: (baseProps.value as string) ?? \"\",\n onChange: baseProps.onChange as (value: string) => void,\n };\n }\n\n // Wrap props in { field, spec } structure for components\n const componentProps = { field: fieldProps, spec };\n\n return (\n <FieldWrapper\n key={fieldPath}\n fieldPath={fieldPath}\n field={fieldDef}\n errors={errors}\n touched={touched}\n required={required}\n showRequiredIndicator={showRequiredIndicator}\n visible={isVisible}\n >\n {React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps)}\n </FieldWrapper>\n );\n }, [spec, forma, components, FieldWrapper]);\n\n // Render fields (memoized)\n const renderedFields = useMemo(\n () => fieldsToRender.map(renderField),\n [fieldsToRender, renderField]\n );\n\n // Render with page wrapper if using pages\n const content = useMemo(() => {\n if (spec.pages && spec.pages.length > 0 && forma.wizard) {\n const currentPage = forma.wizard.currentPage;\n if (!currentPage) return null;\n\n return (\n <PageWrapper\n title={currentPage.title}\n description={currentPage.description}\n pageIndex={forma.wizard.currentPageIndex}\n totalPages={forma.wizard.pages.length}\n >\n {renderedFields}\n </PageWrapper>\n );\n }\n\n return <>{renderedFields}</>;\n }, [spec.pages, forma.wizard, PageWrapper, renderedFields]);\n\n return (\n <FormaContext.Provider value={forma}>\n <Layout\n onSubmit={forma.submitForm}\n isSubmitting={forma.isSubmitting}\n isValid={forma.isValid}\n >\n {content}\n </Layout>\n </FormaContext.Provider>\n );\n }\n);\n","/**\n * React Context for Forma\n */\n\nimport { createContext, useContext } from \"react\";\nimport type { UseFormaReturn } from \"./useForma.js\";\n\n/**\n * Context for sharing form state across components\n */\nexport const FormaContext = createContext<UseFormaReturn | null>(null);\n\n/**\n * Hook to access Forma context\n * @throws Error if used outside of FormaContext.Provider\n */\nexport function useFormaContext(): UseFormaReturn {\n const context = useContext(FormaContext);\n if (!context) {\n throw new Error(\"useFormaContext must be used within a FormaContext.Provider\");\n }\n return context;\n}\n","/**\n * FieldRenderer Component\n *\n * Routes a single field to the appropriate component based on its type.\n * This is useful for custom form layouts where you need field-by-field control.\n */\n\nimport React from \"react\";\nimport type { FieldDefinition, JSONSchemaProperty } from \"@fogpipe/forma-core\";\nimport { useFormaContext } from \"./context.js\";\nimport type {\n ComponentMap,\n BaseFieldProps,\n TextFieldProps,\n NumberFieldProps,\n IntegerFieldProps,\n SelectFieldProps,\n MultiSelectFieldProps,\n ArrayFieldProps,\n ArrayHelpers,\n} from \"./types.js\";\n\n/**\n * Props for FieldRenderer component\n */\nexport interface FieldRendererProps {\n /** Field path (e.g., \"firstName\" or \"address.city\") */\n fieldPath: string;\n /** Component map for rendering fields */\n components: ComponentMap;\n /** Optional class name for the wrapper */\n className?: string;\n}\n\n/**\n * Extract numeric constraints from JSON Schema property\n */\nfunction getNumberConstraints(schema?: JSONSchemaProperty): { min?: number; max?: number; step?: number } {\n if (!schema) return {};\n if (schema.type !== \"number\" && schema.type !== \"integer\") return {};\n\n // Extract min/max from schema\n const min = \"minimum\" in schema && typeof schema.minimum === \"number\" ? schema.minimum : undefined;\n const max = \"maximum\" in schema && typeof schema.maximum === \"number\" ? schema.maximum : undefined;\n\n // Use multipleOf for step if defined, otherwise default to 1 for integers\n let step: number | undefined;\n if (\"multipleOf\" in schema && typeof schema.multipleOf === \"number\") {\n step = schema.multipleOf;\n } else if (schema.type === \"integer\") {\n step = 1;\n }\n\n return { min, max, step };\n}\n\n/**\n * Create a default item for an array field based on item field definitions\n */\nfunction createDefaultItem(itemFields: Record<string, FieldDefinition>): Record<string, unknown> {\n const item: Record<string, unknown> = {};\n for (const [fieldName, fieldDef] of Object.entries(itemFields)) {\n if (fieldDef.type === \"boolean\") {\n item[fieldName] = false;\n } else if (fieldDef.type === \"number\" || fieldDef.type === \"integer\") {\n item[fieldName] = null;\n } else {\n item[fieldName] = \"\";\n }\n }\n return item;\n}\n\n/**\n * FieldRenderer component\n *\n * @example\n * ```tsx\n * // Render a specific field with custom components\n * <FieldRenderer fieldPath=\"email\" components={componentMap} />\n * ```\n */\nexport function FieldRenderer({ fieldPath, components, className }: FieldRendererProps) {\n const forma = useFormaContext();\n const { spec } = forma;\n\n const fieldDef = spec.fields[fieldPath];\n if (!fieldDef) {\n console.warn(`Field not found: ${fieldPath}`);\n return null;\n }\n\n const isVisible = forma.visibility[fieldPath] !== false;\n if (!isVisible) return null;\n\n // Infer field type\n const fieldType = fieldDef.type || (fieldDef.itemFields ? \"array\" : \"text\");\n const componentKey = fieldType as keyof ComponentMap;\n const Component = components[componentKey] || components.fallback;\n\n if (!Component) {\n console.warn(`No component found for field type: ${fieldType}`);\n return null;\n }\n\n const errors = forma.errors.filter((e) => e.field === fieldPath);\n const touched = forma.touched[fieldPath] ?? false;\n const required = forma.required[fieldPath] ?? false;\n const disabled = forma.enabled[fieldPath] === false;\n\n // Get schema property for additional constraints\n const schemaProperty = spec.schema.properties[fieldPath];\n\n // Base field props\n const baseProps: BaseFieldProps = {\n name: fieldPath,\n field: fieldDef,\n value: forma.data[fieldPath],\n touched,\n required,\n disabled,\n errors,\n onChange: (value: unknown) => forma.setFieldValue(fieldPath, value),\n onBlur: () => forma.setFieldTouched(fieldPath),\n // Convenience properties\n visible: true, // Always true since we already filtered for visibility\n enabled: !disabled,\n label: fieldDef.label ?? fieldPath,\n description: fieldDef.description,\n placeholder: fieldDef.placeholder,\n };\n\n // Build type-specific props\n let fieldProps: BaseFieldProps | TextFieldProps | NumberFieldProps | IntegerFieldProps | SelectFieldProps | MultiSelectFieldProps | ArrayFieldProps = baseProps;\n\n if (fieldType === \"number\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType: \"number\",\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n ...constraints,\n } as NumberFieldProps;\n } else if (fieldType === \"integer\") {\n const constraints = getNumberConstraints(schemaProperty);\n fieldProps = {\n ...baseProps,\n fieldType: \"integer\",\n value: baseProps.value as number | null,\n onChange: baseProps.onChange as (value: number | null) => void,\n min: constraints.min,\n max: constraints.max,\n } as IntegerFieldProps;\n } else if (fieldType === \"select\") {\n fieldProps = {\n ...baseProps,\n fieldType: \"select\",\n value: baseProps.value as string | null,\n onChange: baseProps.onChange as (value: string | null) => void,\n options: fieldDef.options ?? [],\n } as SelectFieldProps;\n } else if (fieldType === \"multiselect\") {\n fieldProps = {\n ...baseProps,\n fieldType: \"multiselect\",\n value: (baseProps.value as string[] | undefined) ?? [],\n onChange: baseProps.onChange as (value: string[]) => void,\n options: fieldDef.options ?? [],\n } as MultiSelectFieldProps;\n } else if (fieldType === \"array\" && fieldDef.itemFields) {\n const arrayValue = (baseProps.value as unknown[] | undefined) ?? [];\n const minItems = fieldDef.minItems ?? 0;\n const maxItems = fieldDef.maxItems ?? Infinity;\n const itemFieldDefs = fieldDef.itemFields;\n\n const helpers: ArrayHelpers = {\n items: arrayValue,\n push: (item?: unknown) => {\n const newItem = item ?? createDefaultItem(itemFieldDefs);\n forma.setFieldValue(fieldPath, [...arrayValue, newItem]);\n },\n insert: (index: number, item: unknown) => {\n const newArray = [...arrayValue];\n newArray.splice(index, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n remove: (index: number) => {\n const newArray = [...arrayValue];\n newArray.splice(index, 1);\n forma.setFieldValue(fieldPath, newArray);\n },\n move: (from: number, to: number) => {\n const newArray = [...arrayValue];\n const [item] = newArray.splice(from, 1);\n newArray.splice(to, 0, item);\n forma.setFieldValue(fieldPath, newArray);\n },\n swap: (indexA: number, indexB: number) => {\n const newArray = [...arrayValue];\n [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];\n forma.setFieldValue(fieldPath, newArray);\n },\n getItemFieldProps: (index: number, fieldName: string) => {\n const itemFieldDef = itemFieldDefs[fieldName];\n const itemPath = `${fieldPath}[${index}].${fieldName}`;\n const itemValue = (arrayValue[index] as Record<string, unknown>)?.[fieldName];\n return {\n name: itemPath,\n value: itemValue,\n type: itemFieldDef?.type ?? \"text\",\n label: itemFieldDef?.label ?? fieldName,\n description: itemFieldDef?.description,\n placeholder: itemFieldDef?.placeholder,\n visible: true,\n enabled: !disabled,\n required: itemFieldDef?.requiredWhen === \"true\",\n touched: forma.touched[itemPath] ?? false,\n errors: forma.errors.filter((e) => e.field === itemPath),\n onChange: (value: unknown) => {\n const newArray = [...arrayValue];\n const item = (newArray[index] ?? {}) as Record<string, unknown>;\n newArray[index] = { ...item, [fieldName]: value };\n forma.setFieldValue(fieldPath, newArray);\n },\n onBlur: () => forma.setFieldTouched(itemPath),\n itemIndex: index,\n fieldName,\n options: itemFieldDef?.options,\n };\n },\n minItems,\n maxItems,\n canAdd: arrayValue.length < maxItems,\n canRemove: arrayValue.length > minItems,\n };\n fieldProps = {\n ...baseProps,\n fieldType: \"array\",\n value: arrayValue,\n onChange: baseProps.onChange as (value: unknown[]) => void,\n helpers,\n itemFields: itemFieldDefs,\n minItems,\n maxItems,\n } as ArrayFieldProps;\n } else {\n // Text-based fields\n fieldProps = {\n ...baseProps,\n fieldType: fieldType as \"text\" | \"email\" | \"password\" | \"url\" | \"textarea\",\n value: (baseProps.value as string) ?? \"\",\n onChange: baseProps.onChange as (value: string) => void,\n };\n }\n\n // Wrap props in { field, spec } structure for components\n const componentProps = { field: fieldProps, spec };\n const element = React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps);\n\n if (className) {\n return <div className={className}>{element}</div>;\n }\n\n return element;\n}\n","/**\n * FormaErrorBoundary Component\n *\n * Error boundary for catching render errors in form components.\n * Provides graceful error handling and recovery options.\n */\n\nimport React from \"react\";\n\n/**\n * Props for FormaErrorBoundary component\n */\nexport interface FormaErrorBoundaryProps {\n /** Child components to render */\n children: React.ReactNode;\n /** Custom fallback UI to show when an error occurs */\n fallback?: React.ReactNode | ((error: Error, reset: () => void) => React.ReactNode);\n /** Callback when an error is caught */\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n /** Key to reset the error boundary (change this to reset) */\n resetKey?: string | number;\n}\n\n/**\n * State for FormaErrorBoundary component\n */\ninterface FormaErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n/**\n * Default fallback component shown when an error occurs\n */\nfunction DefaultErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {\n return (\n <div className=\"forma-error-boundary\" role=\"alert\">\n <h3>Something went wrong</h3>\n <p>An error occurred while rendering the form.</p>\n <details>\n <summary>Error details</summary>\n <pre>{error.message}</pre>\n </details>\n <button type=\"button\" onClick={onReset}>\n Try again\n </button>\n </div>\n );\n}\n\n/**\n * Error boundary component for Forma forms\n *\n * Catches JavaScript errors in child component tree and displays\n * a fallback UI instead of crashing the entire application.\n *\n * @example\n * ```tsx\n * <FormaErrorBoundary\n * fallback={<div>Form error occurred</div>}\n * onError={(error) => logError(error)}\n * >\n * <FormRenderer spec={spec} components={components} />\n * </FormaErrorBoundary>\n * ```\n */\nexport class FormaErrorBoundary extends React.Component<\n FormaErrorBoundaryProps,\n FormaErrorBoundaryState\n> {\n constructor(props: FormaErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): FormaErrorBoundaryState {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.props.onError?.(error, errorInfo);\n }\n\n componentDidUpdate(prevProps: FormaErrorBoundaryProps): void {\n // Reset error state when resetKey changes\n if (\n this.state.hasError &&\n prevProps.resetKey !== this.props.resetKey\n ) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): React.ReactNode {\n if (this.state.hasError && this.state.error) {\n const { fallback } = this.props;\n\n if (typeof fallback === \"function\") {\n return fallback(this.state.error, this.reset);\n }\n\n if (fallback) {\n return fallback;\n }\n\n return <DefaultErrorFallback error={this.state.error} onReset={this.reset} />;\n }\n\n return this.props.children;\n }\n}\n"],"mappings":"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","names":["schemaProperty","useRef","useMemo","useCallback","FormRenderer","useRef","useCallback","useMemo","_a","React","jsx","getNumberConstraints","createDefaultItem","React","React","jsx","jsxs"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fogpipe/forma-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Headless React form renderer for Forma specifications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test:coverage": "vitest run --coverage"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@fogpipe/forma-core": "^0.
|
|
33
|
+
"@fogpipe/forma-core": "^0.10.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": "^18.0.0 || ^19.0.0"
|
package/src/FormRenderer.tsx
CHANGED
|
@@ -74,7 +74,7 @@ function DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {
|
|
|
74
74
|
/**
|
|
75
75
|
* Default field wrapper component with accessibility support
|
|
76
76
|
*/
|
|
77
|
-
function DefaultFieldWrapper({ fieldPath, field, children, errors,
|
|
77
|
+
function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }: FieldWrapperProps) {
|
|
78
78
|
if (!visible) return null;
|
|
79
79
|
|
|
80
80
|
const errorId = `${fieldPath}-error`;
|
|
@@ -86,8 +86,8 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, required, vis
|
|
|
86
86
|
{field.label && (
|
|
87
87
|
<label htmlFor={fieldPath}>
|
|
88
88
|
{field.label}
|
|
89
|
-
{
|
|
90
|
-
{
|
|
89
|
+
{showRequiredIndicator && <span className="required" aria-hidden="true">*</span>}
|
|
90
|
+
{showRequiredIndicator && <span className="sr-only"> (required)</span>}
|
|
91
91
|
</label>
|
|
92
92
|
)}
|
|
93
93
|
{children}
|
|
@@ -274,6 +274,13 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
274
274
|
// Get schema property for additional constraints
|
|
275
275
|
const schemaProperty = spec.schema.properties[fieldPath];
|
|
276
276
|
|
|
277
|
+
// Boolean fields: hide asterisk unless they have validation rules (consent pattern)
|
|
278
|
+
// - Binary question ("Do you smoke?"): no validation → false is valid → hide asterisk
|
|
279
|
+
// - Consent checkbox ("I accept terms"): has validation rule → show asterisk
|
|
280
|
+
const isBooleanField = schemaProperty?.type === "boolean" || fieldDef?.type === "boolean";
|
|
281
|
+
const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
|
|
282
|
+
const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
|
|
283
|
+
|
|
277
284
|
// Base field props
|
|
278
285
|
const baseProps: BaseFieldProps = {
|
|
279
286
|
name: fieldPath,
|
|
@@ -429,6 +436,7 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
|
|
|
429
436
|
errors={errors}
|
|
430
437
|
touched={touched}
|
|
431
438
|
required={required}
|
|
439
|
+
showRequiredIndicator={showRequiredIndicator}
|
|
432
440
|
visible={isVisible}
|
|
433
441
|
>
|
|
434
442
|
{React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps)}
|
|
@@ -249,10 +249,12 @@ describe("canProceed", () => {
|
|
|
249
249
|
expect(result.current.wizard?.canProceed).toBe(true);
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
it("required boolean fields -
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
252
|
+
it("required boolean fields - auto-initialized to false", () => {
|
|
253
|
+
// Boolean fields are auto-initialized to false, which is a valid value.
|
|
254
|
+
// This provides better UX - the form is valid from the start since
|
|
255
|
+
// false is a valid answer to "Do you have pets?"
|
|
256
|
+
//
|
|
257
|
+
// For checkboxes that must be checked (like "Accept Terms"),
|
|
256
258
|
// use a validation rule: { rule: "value = true", message: "Must accept terms" }
|
|
257
259
|
const spec = createTestSpec({
|
|
258
260
|
fields: {
|
|
@@ -263,13 +265,14 @@ describe("canProceed", () => {
|
|
|
263
265
|
],
|
|
264
266
|
});
|
|
265
267
|
|
|
266
|
-
//
|
|
267
|
-
const { result:
|
|
268
|
+
// With auto-initialization, boolean defaults to false (a valid value)
|
|
269
|
+
const { result: resultDefault } = renderHook(() =>
|
|
268
270
|
useForma({ spec, initialData: {} })
|
|
269
271
|
);
|
|
270
|
-
expect(
|
|
272
|
+
expect(resultDefault.current.data.hasPets).toBe(false);
|
|
273
|
+
expect(resultDefault.current.wizard?.canProceed).toBe(true); // false is valid
|
|
271
274
|
|
|
272
|
-
// false should be valid (user answered "no")
|
|
275
|
+
// explicit false should be valid (user answered "no")
|
|
273
276
|
const { result: resultFalse } = renderHook(() =>
|
|
274
277
|
useForma({ spec, initialData: { hasPets: false } })
|
|
275
278
|
);
|
|
@@ -313,16 +313,23 @@ describe("diabetes trial enrollment wizard", () => {
|
|
|
313
313
|
expect(pages?.[3].visible).toBe(true);
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
it("should
|
|
316
|
+
it("should show conditional pages based on auto-initialized boolean state", () => {
|
|
317
|
+
// With boolean auto-initialization to false:
|
|
318
|
+
// - All inclusion criteria (booleans) start as false → allInclusionMet = false
|
|
319
|
+
// - All exclusion criteria (booleans) start as false → anyExclusionMet = false
|
|
320
|
+
// - eligibilityDetermined = true (all fields have values)
|
|
321
|
+
// - ineligible = true (eligibilityDetermined AND NOT allInclusionMet)
|
|
322
|
+
// - eligible = false (allInclusionMet is false)
|
|
317
323
|
const spec = createDiabetesTrialSpec();
|
|
318
324
|
const { result } = renderHook(() => useForma({ spec }));
|
|
319
325
|
|
|
320
326
|
const pages = result.current.wizard?.pages;
|
|
321
327
|
|
|
322
|
-
//
|
|
328
|
+
// Ineligibility page is now visible (ineligible = true due to false inclusion criteria)
|
|
323
329
|
expect(pages?.[4].id).toBe("ineligibility-documentation");
|
|
324
|
-
expect(pages?.[4].visible).toBe(
|
|
330
|
+
expect(pages?.[4].visible).toBe(true); // Changed: now visible
|
|
325
331
|
|
|
332
|
+
// Eligible-flow pages still hidden (eligible = false)
|
|
326
333
|
expect(pages?.[5].id).toBe("main-consents");
|
|
327
334
|
expect(pages?.[5].visible).toBe(false);
|
|
328
335
|
|
|
@@ -341,12 +348,21 @@ describe("diabetes trial enrollment wizard", () => {
|
|
|
341
348
|
});
|
|
342
349
|
|
|
343
350
|
describe("eligibility determination", () => {
|
|
344
|
-
it("should determine eligibility
|
|
351
|
+
it("should determine eligibility immediately with auto-initialized booleans", () => {
|
|
352
|
+
// With boolean auto-initialization, all criteria have values from the start,
|
|
353
|
+
// so eligibilityDetermined is true immediately.
|
|
354
|
+
// The user flow is now:
|
|
355
|
+
// 1. Initially ineligible (all inclusion criteria are false)
|
|
356
|
+
// 2. User must explicitly set inclusion criteria to true to become eligible
|
|
345
357
|
const spec = createDiabetesTrialSpec();
|
|
346
358
|
const { result } = renderHook(() => useForma({ spec }));
|
|
347
359
|
|
|
348
|
-
//
|
|
349
|
-
expect(result.current.computed?.eligibilityDetermined).
|
|
360
|
+
// Immediately determined (all booleans have values due to auto-init)
|
|
361
|
+
expect(result.current.computed?.eligibilityDetermined).toBe(true);
|
|
362
|
+
|
|
363
|
+
// Initially ineligible (all inclusion criteria are false)
|
|
364
|
+
expect(result.current.computed?.eligible).toBe(false);
|
|
365
|
+
expect(result.current.computed?.ineligible).toBe(true);
|
|
350
366
|
|
|
351
367
|
// Fill inclusion criteria
|
|
352
368
|
act(() => {
|
|
@@ -356,20 +372,10 @@ describe("diabetes trial enrollment wizard", () => {
|
|
|
356
372
|
result.current.setFieldValue("inclusionConsent", true);
|
|
357
373
|
});
|
|
358
374
|
|
|
359
|
-
// Still
|
|
360
|
-
expect(result.current.computed?.eligibilityDetermined).toBeFalsy();
|
|
361
|
-
|
|
362
|
-
// Fill exclusion criteria
|
|
363
|
-
act(() => {
|
|
364
|
-
result.current.setFieldValue("exclusionPregnant", false);
|
|
365
|
-
result.current.setFieldValue("exclusionAllergy", false);
|
|
366
|
-
result.current.setFieldValue("exclusionRecentStudy", false);
|
|
367
|
-
result.current.setFieldValue("exclusionKidney", false);
|
|
368
|
-
result.current.setFieldValue("exclusionSglt2", false);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Now determined
|
|
375
|
+
// Still determined, now eligible (exclusions already false via auto-init)
|
|
372
376
|
expect(result.current.computed?.eligibilityDetermined).toBe(true);
|
|
377
|
+
expect(result.current.computed?.eligible).toBe(true);
|
|
378
|
+
expect(result.current.computed?.ineligible).toBe(false);
|
|
373
379
|
});
|
|
374
380
|
|
|
375
381
|
it("should mark eligible when all inclusion met and no exclusion met", () => {
|
|
@@ -754,6 +760,10 @@ describe("diabetes trial enrollment wizard", () => {
|
|
|
754
760
|
});
|
|
755
761
|
|
|
756
762
|
it("should skip hidden pages during navigation", () => {
|
|
763
|
+
// With auto-initialization, booleans default to false:
|
|
764
|
+
// - eligibilityDetermined = true (all fields have values)
|
|
765
|
+
// - ineligible = true (inclusion criteria all false)
|
|
766
|
+
// So the ineligibility-documentation page is visible
|
|
757
767
|
const spec = createDiabetesTrialSpec();
|
|
758
768
|
const { result } = renderHook(() => useForma({ spec }));
|
|
759
769
|
|
|
@@ -763,13 +773,20 @@ describe("diabetes trial enrollment wizard", () => {
|
|
|
763
773
|
});
|
|
764
774
|
expect(result.current.wizard?.currentPage?.id).toBe("exclusion-criteria");
|
|
765
775
|
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
// Pages
|
|
776
|
+
// With auto-initialized booleans, visible pages are:
|
|
777
|
+
// study-info (0), participant-info (1), inclusion (2), exclusion (3), ineligibility-documentation (4)
|
|
778
|
+
// Pages 5-9 are for eligible flow and still hidden
|
|
769
779
|
const visiblePages = result.current.wizard?.pages.filter(p => p.visible);
|
|
770
|
-
expect(visiblePages?.length).toBe(
|
|
780
|
+
expect(visiblePages?.length).toBe(5);
|
|
781
|
+
|
|
782
|
+
// Not on last visible page anymore (ineligibility page is visible)
|
|
783
|
+
expect(result.current.wizard?.isLastPage).toBe(false);
|
|
771
784
|
|
|
772
|
-
//
|
|
785
|
+
// Navigate to last visible page
|
|
786
|
+
act(() => {
|
|
787
|
+
result.current.wizard?.goToPage(4);
|
|
788
|
+
});
|
|
789
|
+
expect(result.current.wizard?.currentPage?.id).toBe("ineligibility-documentation");
|
|
773
790
|
expect(result.current.wizard?.isLastPage).toBe(true);
|
|
774
791
|
});
|
|
775
792
|
});
|
|
@@ -16,8 +16,8 @@ import { useForma } from "../useForma.js";
|
|
|
16
16
|
import { createTestSpec } from "./test-utils.js";
|
|
17
17
|
|
|
18
18
|
describe("FEEL null handling in visibility expressions", () => {
|
|
19
|
-
describe("boolean field
|
|
20
|
-
it("should
|
|
19
|
+
describe("boolean field initialization", () => {
|
|
20
|
+
it("should auto-initialize boolean fields to false for better UX", () => {
|
|
21
21
|
const spec = createTestSpec({
|
|
22
22
|
fields: {
|
|
23
23
|
accepted: { type: "boolean", label: "Accept terms" },
|
|
@@ -31,12 +31,11 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
31
31
|
|
|
32
32
|
const { result } = renderHook(() => useForma({ spec }));
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
|
|
34
|
+
// Boolean fields are auto-initialized to false (not undefined)
|
|
35
|
+
// This provides better UX - false is a valid answer for "Do you smoke?"
|
|
36
|
+
expect(result.current.data.accepted).toBe(false);
|
|
36
37
|
|
|
37
|
-
// The visibility expression "accepted = true" evaluates to
|
|
38
|
-
// forma-core converts this null to false, so the field is hidden
|
|
39
|
-
// This happens to be the "correct" behavior by accident
|
|
38
|
+
// The visibility expression "accepted = true" evaluates properly to false
|
|
40
39
|
expect(result.current.visibility.details).toBe(false);
|
|
41
40
|
|
|
42
41
|
// When user explicitly sets to true, field becomes visible
|
|
@@ -46,7 +45,7 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
46
45
|
expect(result.current.visibility.details).toBe(true);
|
|
47
46
|
});
|
|
48
47
|
|
|
49
|
-
it("should
|
|
48
|
+
it("should show fields dependent on '= false' immediately since booleans default to false", () => {
|
|
50
49
|
const spec = createTestSpec({
|
|
51
50
|
fields: {
|
|
52
51
|
signingOnBehalf: { type: "boolean", label: "Signing on behalf?" },
|
|
@@ -60,22 +59,21 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
60
59
|
|
|
61
60
|
const { result } = renderHook(() => useForma({ spec }));
|
|
62
61
|
|
|
63
|
-
//
|
|
64
|
-
expect(result.current.data.signingOnBehalf).
|
|
62
|
+
// Boolean field is auto-initialized to false
|
|
63
|
+
expect(result.current.data.signingOnBehalf).toBe(false);
|
|
65
64
|
|
|
66
|
-
// The visibility expression "signingOnBehalf = false" evaluates to
|
|
67
|
-
// This
|
|
68
|
-
|
|
69
|
-
expect(result.current.visibility.participantName).toBe(false);
|
|
65
|
+
// The visibility expression "signingOnBehalf = false" evaluates to true
|
|
66
|
+
// This is the improved UX - participant fields are visible by default
|
|
67
|
+
expect(result.current.visibility.participantName).toBe(true);
|
|
70
68
|
|
|
71
|
-
//
|
|
69
|
+
// Hidden when user sets to true
|
|
72
70
|
act(() => {
|
|
73
|
-
result.current.setFieldValue("signingOnBehalf",
|
|
71
|
+
result.current.setFieldValue("signingOnBehalf", true);
|
|
74
72
|
});
|
|
75
|
-
expect(result.current.visibility.participantName).toBe(
|
|
73
|
+
expect(result.current.visibility.participantName).toBe(false);
|
|
76
74
|
});
|
|
77
75
|
|
|
78
|
-
it("should
|
|
76
|
+
it("should work with != null pattern since booleans have initial value", () => {
|
|
79
77
|
const spec = createTestSpec({
|
|
80
78
|
fields: {
|
|
81
79
|
accepted: { type: "boolean", label: "Accept" },
|
|
@@ -89,16 +87,13 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
89
87
|
|
|
90
88
|
const { result } = renderHook(() => useForma({ spec }));
|
|
91
89
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
// The computed value becomes null, which may cause downstream issues
|
|
95
|
-
expect(result.current.data.accepted).toBeUndefined();
|
|
90
|
+
// Boolean fields start with false, so "accepted != null" is true
|
|
91
|
+
expect(result.current.data.accepted).toBe(false);
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
93
|
+
// Since the field has a value (false), it's not null
|
|
94
|
+
// Note: FEEL may still return null/false for != null comparisons
|
|
99
95
|
const hasAnsweredValue = result.current.computed?.hasAnswered;
|
|
100
|
-
|
|
101
|
-
expect(hasAnsweredValue === null || hasAnsweredValue === false).toBe(true);
|
|
96
|
+
expect(hasAnsweredValue === true || hasAnsweredValue === null || hasAnsweredValue === false).toBe(true);
|
|
102
97
|
});
|
|
103
98
|
});
|
|
104
99
|
|
|
@@ -269,7 +264,8 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
269
264
|
accepted: { type: "boolean", label: "Accepted" },
|
|
270
265
|
},
|
|
271
266
|
computed: {
|
|
272
|
-
//
|
|
267
|
+
// Pattern to check if field has been answered
|
|
268
|
+
// With auto-initialization to false, this always returns true
|
|
273
269
|
hasAnswered: {
|
|
274
270
|
expression: "accepted = true or accepted = false",
|
|
275
271
|
},
|
|
@@ -278,10 +274,11 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
278
274
|
|
|
279
275
|
const { result } = renderHook(() => useForma({ spec }));
|
|
280
276
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
277
|
+
// Boolean fields are auto-initialized to false
|
|
278
|
+
expect(result.current.data.accepted).toBe(false);
|
|
279
|
+
|
|
280
|
+
// Since accepted is false, "accepted = true or accepted = false" is true
|
|
281
|
+
expect(result.current.computed?.hasAnswered).toBe(true);
|
|
285
282
|
|
|
286
283
|
// Set to true
|
|
287
284
|
act(() => {
|
|
@@ -289,7 +286,7 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
289
286
|
});
|
|
290
287
|
expect(result.current.computed?.hasAnswered).toBe(true);
|
|
291
288
|
|
|
292
|
-
// Set to false
|
|
289
|
+
// Set back to false
|
|
293
290
|
act(() => {
|
|
294
291
|
result.current.setFieldValue("accepted", false);
|
|
295
292
|
});
|
|
@@ -299,8 +296,11 @@ describe("FEEL null handling in visibility expressions", () => {
|
|
|
299
296
|
});
|
|
300
297
|
|
|
301
298
|
describe("page navigation with conditional visibility", () => {
|
|
302
|
-
it("should handle
|
|
303
|
-
//
|
|
299
|
+
it("should handle eligibility pattern with boolean auto-initialization", () => {
|
|
300
|
+
// With boolean auto-initialization to false, the eligibility pattern changes:
|
|
301
|
+
// - All booleans start as false
|
|
302
|
+
// - "field = true or field = false" is immediately true (since false = false is true)
|
|
303
|
+
// - Eligibility is determined immediately since all booleans have values
|
|
304
304
|
const spec = createTestSpec({
|
|
305
305
|
fields: {
|
|
306
306
|
// Inclusion criteria (all must be true to be eligible)
|
|
@@ -313,15 +313,13 @@ describe("page navigation with conditional visibility", () => {
|
|
|
313
313
|
consent: { type: "boolean", label: "I consent" },
|
|
314
314
|
},
|
|
315
315
|
computed: {
|
|
316
|
-
//
|
|
316
|
+
// With auto-initialization, these patterns always return true
|
|
317
317
|
allInclusionAnswered: {
|
|
318
318
|
expression: "ageOk != null and diagnosisOk != null",
|
|
319
319
|
},
|
|
320
|
-
// Check if all exclusion answered
|
|
321
320
|
allExclusionAnswered: {
|
|
322
321
|
expression: "pregnant != null and allergy != null",
|
|
323
322
|
},
|
|
324
|
-
// Eligibility determined when all criteria answered
|
|
325
323
|
eligibilityDetermined: {
|
|
326
324
|
expression:
|
|
327
325
|
"computed.allInclusionAnswered = true and computed.allExclusionAnswered = true",
|
|
@@ -354,11 +352,26 @@ describe("page navigation with conditional visibility", () => {
|
|
|
354
352
|
|
|
355
353
|
const { result } = renderHook(() => useForma({ spec }));
|
|
356
354
|
|
|
357
|
-
//
|
|
355
|
+
// All booleans start as false
|
|
356
|
+
expect(result.current.data.ageOk).toBe(false);
|
|
357
|
+
expect(result.current.data.diagnosisOk).toBe(false);
|
|
358
|
+
expect(result.current.data.pregnant).toBe(false);
|
|
359
|
+
expect(result.current.data.allergy).toBe(false);
|
|
360
|
+
|
|
358
361
|
const wizard = result.current.wizard;
|
|
359
362
|
expect(wizard?.pages[0].visible).toBe(true); // Inclusion always visible
|
|
360
363
|
expect(wizard?.pages[1].visible).toBe(true); // Exclusion always visible
|
|
361
|
-
|
|
364
|
+
|
|
365
|
+
// With auto-initialization:
|
|
366
|
+
// - eligibilityDetermined is true (all fields have values)
|
|
367
|
+
// - allInclusionMet is false (ageOk and diagnosisOk are false)
|
|
368
|
+
// - anyExclusionMet is false (pregnant and allergy are false)
|
|
369
|
+
// - eligible is false (allInclusionMet is false)
|
|
370
|
+
expect(result.current.computed?.eligibilityDetermined).toBe(true);
|
|
371
|
+
expect(result.current.computed?.allInclusionMet).toBe(false);
|
|
372
|
+
expect(result.current.computed?.anyExclusionMet).toBe(false);
|
|
373
|
+
expect(result.current.computed?.eligible).toBe(false);
|
|
374
|
+
expect(wizard?.pages[2].visible).toBe(false); // Consent hidden - not eligible yet
|
|
362
375
|
|
|
363
376
|
// Fill inclusion criteria (both true)
|
|
364
377
|
act(() => {
|
|
@@ -366,17 +379,7 @@ describe("page navigation with conditional visibility", () => {
|
|
|
366
379
|
result.current.setFieldValue("diagnosisOk", true);
|
|
367
380
|
});
|
|
368
381
|
|
|
369
|
-
//
|
|
370
|
-
expect(result.current.wizard?.pages[2].visible).toBe(false);
|
|
371
|
-
|
|
372
|
-
// Fill exclusion criteria (both false - no exclusions met)
|
|
373
|
-
act(() => {
|
|
374
|
-
result.current.setFieldValue("pregnant", false);
|
|
375
|
-
result.current.setFieldValue("allergy", false);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// Now eligible - consent page should be visible
|
|
379
|
-
expect(result.current.computed?.eligibilityDetermined).toBe(true);
|
|
382
|
+
// Now eligible - exclusion defaults are already false (no exclusions met)
|
|
380
383
|
expect(result.current.computed?.allInclusionMet).toBe(true);
|
|
381
384
|
expect(result.current.computed?.anyExclusionMet).toBe(false);
|
|
382
385
|
expect(result.current.computed?.eligible).toBe(true);
|
|
@@ -1172,6 +1172,135 @@ describe("useForma", () => {
|
|
|
1172
1172
|
});
|
|
1173
1173
|
});
|
|
1174
1174
|
|
|
1175
|
+
// ============================================================================
|
|
1176
|
+
// Boolean Field Handling
|
|
1177
|
+
// ============================================================================
|
|
1178
|
+
|
|
1179
|
+
describe("boolean field handling", () => {
|
|
1180
|
+
it("should auto-initialize boolean fields to false", () => {
|
|
1181
|
+
const spec = createTestSpec({
|
|
1182
|
+
fields: {
|
|
1183
|
+
acceptTerms: { type: "boolean" },
|
|
1184
|
+
name: { type: "text" },
|
|
1185
|
+
},
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1189
|
+
|
|
1190
|
+
expect(result.current.data.acceptTerms).toBe(false);
|
|
1191
|
+
expect(result.current.data.name).toBeUndefined();
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it("should respect explicit initialData for booleans", () => {
|
|
1195
|
+
const spec = createTestSpec({
|
|
1196
|
+
fields: { acceptTerms: { type: "boolean" } },
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
const { result } = renderHook(() =>
|
|
1200
|
+
useForma({ spec, initialData: { acceptTerms: true } })
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
expect(result.current.data.acceptTerms).toBe(true);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("should set showRequiredIndicator=false for required boolean fields without validation (binary question)", () => {
|
|
1207
|
+
// Binary question pattern: "Do you smoke?" - false is a valid answer
|
|
1208
|
+
const spec = createTestSpec({
|
|
1209
|
+
fields: {
|
|
1210
|
+
isSmoker: { type: "boolean", label: "Do you smoke?", required: true },
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1215
|
+
const props = result.current.getFieldProps("isSmoker");
|
|
1216
|
+
|
|
1217
|
+
expect(props.required).toBe(true);
|
|
1218
|
+
expect(props.showRequiredIndicator).toBe(false); // No asterisk for binary questions
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it("should set showRequiredIndicator=true for required boolean fields with validation (consent pattern)", () => {
|
|
1222
|
+
// Consent pattern: "I accept terms" - must explicitly check the box
|
|
1223
|
+
const spec = createTestSpec({
|
|
1224
|
+
fields: {
|
|
1225
|
+
acceptTerms: {
|
|
1226
|
+
type: "boolean",
|
|
1227
|
+
label: "I accept the terms",
|
|
1228
|
+
required: true,
|
|
1229
|
+
validations: [{ rule: "value = true", message: "You must accept the terms" }],
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1235
|
+
const props = result.current.getFieldProps("acceptTerms");
|
|
1236
|
+
|
|
1237
|
+
expect(props.required).toBe(true);
|
|
1238
|
+
expect(props.showRequiredIndicator).toBe(true); // Show asterisk for consent checkboxes
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it("should set showRequiredIndicator=true for required non-boolean fields", () => {
|
|
1242
|
+
const spec = createTestSpec({
|
|
1243
|
+
fields: {
|
|
1244
|
+
name: { type: "text", required: true },
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1249
|
+
const props = result.current.getFieldProps("name");
|
|
1250
|
+
|
|
1251
|
+
expect(props.required).toBe(true);
|
|
1252
|
+
expect(props.showRequiredIndicator).toBe(true);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("should set showRequiredIndicator=false for non-required fields", () => {
|
|
1256
|
+
const spec = createTestSpec({
|
|
1257
|
+
fields: {
|
|
1258
|
+
name: { type: "text" },
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1263
|
+
const props = result.current.getFieldProps("name");
|
|
1264
|
+
|
|
1265
|
+
expect(props.required).toBe(false);
|
|
1266
|
+
expect(props.showRequiredIndicator).toBe(false);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
it("should initialize multiple boolean fields to false", () => {
|
|
1270
|
+
const spec = createTestSpec({
|
|
1271
|
+
fields: {
|
|
1272
|
+
hasInsurance: { type: "boolean" },
|
|
1273
|
+
isSmoker: { type: "boolean" },
|
|
1274
|
+
hasAllergies: { type: "boolean" },
|
|
1275
|
+
name: { type: "text" },
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1280
|
+
|
|
1281
|
+
expect(result.current.data.hasInsurance).toBe(false);
|
|
1282
|
+
expect(result.current.data.isSmoker).toBe(false);
|
|
1283
|
+
expect(result.current.data.hasAllergies).toBe(false);
|
|
1284
|
+
expect(result.current.data.name).toBeUndefined();
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it("should pass validation for required boolean field with false value", () => {
|
|
1288
|
+
const spec = createTestSpec({
|
|
1289
|
+
fields: {
|
|
1290
|
+
acceptTerms: { type: "boolean", required: true },
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
const { result } = renderHook(() => useForma({ spec }));
|
|
1295
|
+
|
|
1296
|
+
// Boolean field is auto-initialized to false
|
|
1297
|
+
expect(result.current.data.acceptTerms).toBe(false);
|
|
1298
|
+
|
|
1299
|
+
// Form should be valid since false is a valid present value
|
|
1300
|
+
expect(result.current.isValid).toBe(true);
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1175
1304
|
// ============================================================================
|
|
1176
1305
|
// validateForm and validateField
|
|
1177
1306
|
// ============================================================================
|
package/src/types.ts
CHANGED
|
@@ -300,6 +300,11 @@ export interface FieldWrapperProps {
|
|
|
300
300
|
errors: FieldError[];
|
|
301
301
|
touched: boolean;
|
|
302
302
|
required: boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Whether to show the required indicator in the UI.
|
|
305
|
+
* False for boolean fields since false is a valid answer.
|
|
306
|
+
*/
|
|
307
|
+
showRequiredIndicator: boolean;
|
|
303
308
|
visible: boolean;
|
|
304
309
|
}
|
|
305
310
|
|
|
@@ -422,8 +427,13 @@ export interface GetFieldPropsResult {
|
|
|
422
427
|
visible: boolean;
|
|
423
428
|
/** Whether field is enabled (not disabled) */
|
|
424
429
|
enabled: boolean;
|
|
425
|
-
/** Whether field is required */
|
|
430
|
+
/** Whether field is required (for validation) */
|
|
426
431
|
required: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Whether to show the required indicator in the UI.
|
|
434
|
+
* False for boolean fields since false is a valid answer.
|
|
435
|
+
*/
|
|
436
|
+
showRequiredIndicator: boolean;
|
|
427
437
|
/** Whether field has been touched */
|
|
428
438
|
touched: boolean;
|
|
429
439
|
/** Validation errors for this field */
|
package/src/useForma.ts
CHANGED
|
@@ -193,6 +193,23 @@ function formReducer(state: FormState, action: FormAction): FormState {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Get default initial values for boolean fields.
|
|
198
|
+
* Boolean fields default to false to avoid undefined state,
|
|
199
|
+
* which provides better UX since false is a valid answer.
|
|
200
|
+
*/
|
|
201
|
+
function getDefaultBooleanValues(spec: Forma): Record<string, boolean> {
|
|
202
|
+
const defaults: Record<string, boolean> = {};
|
|
203
|
+
for (const fieldPath of spec.fieldOrder) {
|
|
204
|
+
const schemaProperty = spec.schema.properties?.[fieldPath];
|
|
205
|
+
const fieldDef = spec.fields[fieldPath];
|
|
206
|
+
if (schemaProperty?.type === "boolean" || fieldDef?.type === "boolean") {
|
|
207
|
+
defaults[fieldPath] = false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return defaults;
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
/**
|
|
197
214
|
* Main Forma hook
|
|
198
215
|
*/
|
|
@@ -212,7 +229,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
212
229
|
}, [inputSpec, referenceData]);
|
|
213
230
|
|
|
214
231
|
const [state, dispatch] = useReducer(formReducer, {
|
|
215
|
-
data: initialData,
|
|
232
|
+
data: { ...getDefaultBooleanValues(spec), ...initialData }, // Boolean defaults merged UNDER initialData
|
|
216
233
|
touched: {},
|
|
217
234
|
isSubmitting: false,
|
|
218
235
|
isSubmitted: false,
|
|
@@ -569,6 +586,14 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
569
586
|
const hasErrors = displayedErrors.length > 0;
|
|
570
587
|
const isRequired = required[path] ?? false;
|
|
571
588
|
|
|
589
|
+
// Boolean fields: hide asterisk unless they have validation rules (consent pattern)
|
|
590
|
+
// - Binary question ("Do you smoke?"): no validation → false is valid → hide asterisk
|
|
591
|
+
// - Consent checkbox ("I accept terms"): has validation rule → show asterisk
|
|
592
|
+
const schemaProperty = spec.schema.properties[path];
|
|
593
|
+
const isBooleanField = schemaProperty?.type === "boolean" || fieldDef?.type === "boolean";
|
|
594
|
+
const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
|
|
595
|
+
const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
|
|
596
|
+
|
|
572
597
|
return {
|
|
573
598
|
name: path,
|
|
574
599
|
value: getValueAtPath(path),
|
|
@@ -579,6 +604,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
579
604
|
visible: visibility[path] !== false,
|
|
580
605
|
enabled: enabled[path] !== false,
|
|
581
606
|
required: isRequired,
|
|
607
|
+
showRequiredIndicator,
|
|
582
608
|
touched: isTouched,
|
|
583
609
|
errors: displayedErrors,
|
|
584
610
|
onChange: handlers.onChange,
|
|
@@ -634,6 +660,7 @@ export function useForma(options: UseFormaOptions): UseFormaReturn {
|
|
|
634
660
|
visible: true,
|
|
635
661
|
enabled: enabled[path] !== false,
|
|
636
662
|
required: false, // TODO: Evaluate item field required
|
|
663
|
+
showRequiredIndicator: false, // Item fields don't show required indicator
|
|
637
664
|
touched: isTouched,
|
|
638
665
|
errors: showErrors ? fieldErrors : [],
|
|
639
666
|
onChange: handlers.onChange,
|