@fogpipe/forma-react 0.11.2 → 0.12.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,18 @@
1
1
  // src/useForma.ts
2
- import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+ import { isAdornableField } from "@fogpipe/forma-core";
3
11
  import {
4
12
  getVisibility,
5
13
  getRequired,
6
14
  getEnabled,
15
+ getReadonly,
7
16
  validate,
8
17
  calculate,
9
18
  getPageVisibility,
@@ -64,7 +73,15 @@ function getDefaultBooleanValues(spec) {
64
73
  return defaults;
65
74
  }
66
75
  function useForma(options) {
67
- const { spec: inputSpec, initialData = {}, onSubmit, onChange, validateOn = "blur", referenceData, validationDebounceMs = 0 } = options;
76
+ const {
77
+ spec: inputSpec,
78
+ initialData = {},
79
+ onSubmit,
80
+ onChange,
81
+ validateOn = "blur",
82
+ referenceData,
83
+ validationDebounceMs = 0
84
+ } = options;
68
85
  const spec = useMemo(() => {
69
86
  if (!referenceData) return inputSpec;
70
87
  return {
@@ -103,6 +120,10 @@ function useForma(options) {
103
120
  () => getEnabled(state.data, spec, { computed }),
104
121
  [state.data, spec, computed]
105
122
  );
123
+ const readonly = useMemo(
124
+ () => getReadonly(state.data, spec, { computed }),
125
+ [state.data, spec, computed]
126
+ );
106
127
  const optionsVisibility = useMemo(
107
128
  () => getOptionsVisibility(state.data, spec, { computed }),
108
129
  [state.data, spec, computed]
@@ -130,33 +151,39 @@ function useForma(options) {
130
151
  hasInitialized.current = true;
131
152
  }
132
153
  }, [state.data, computed, onChange]);
133
- const setNestedValue = useCallback((path, value) => {
134
- const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
135
- if (parts.length === 1) {
136
- dispatch({ type: "SET_FIELD_VALUE", field: path, value });
137
- return;
138
- }
139
- const buildNestedObject = (data, pathParts, val) => {
140
- const result = { ...data };
141
- let current = result;
142
- for (let i = 0; i < pathParts.length - 1; i++) {
143
- const part = pathParts[i];
144
- const nextPart = pathParts[i + 1];
145
- const isNextArrayIndex = /^\d+$/.test(nextPart);
146
- if (current[part] === void 0) {
147
- current[part] = isNextArrayIndex ? [] : {};
148
- } else if (Array.isArray(current[part])) {
149
- current[part] = [...current[part]];
150
- } else {
151
- current[part] = { ...current[part] };
152
- }
153
- current = current[part];
154
+ const setNestedValue = useCallback(
155
+ (path, value) => {
156
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
157
+ if (parts.length === 1) {
158
+ dispatch({ type: "SET_FIELD_VALUE", field: path, value });
159
+ return;
154
160
  }
155
- current[pathParts[pathParts.length - 1]] = val;
156
- return result;
157
- };
158
- dispatch({ type: "SET_VALUES", values: buildNestedObject(state.data, parts, value) });
159
- }, [state.data]);
161
+ const buildNestedObject = (data, pathParts, val) => {
162
+ const result = { ...data };
163
+ let current = result;
164
+ for (let i = 0; i < pathParts.length - 1; i++) {
165
+ const part = pathParts[i];
166
+ const nextPart = pathParts[i + 1];
167
+ const isNextArrayIndex = /^\d+$/.test(nextPart);
168
+ if (current[part] === void 0) {
169
+ current[part] = isNextArrayIndex ? [] : {};
170
+ } else if (Array.isArray(current[part])) {
171
+ current[part] = [...current[part]];
172
+ } else {
173
+ current[part] = { ...current[part] };
174
+ }
175
+ current = current[part];
176
+ }
177
+ current[pathParts[pathParts.length - 1]] = val;
178
+ return result;
179
+ };
180
+ dispatch({
181
+ type: "SET_VALUES",
182
+ values: buildNestedObject(state.data, parts, value)
183
+ });
184
+ },
185
+ [state.data]
186
+ );
160
187
  const setFieldValue = useCallback(
161
188
  (path, value) => {
162
189
  setNestedValue(path, value);
@@ -207,7 +234,10 @@ function useForma(options) {
207
234
  }));
208
235
  const visiblePages = pages.filter((p) => p.visible);
209
236
  const maxPageIndex = Math.max(0, visiblePages.length - 1);
210
- const clampedPageIndex = Math.min(Math.max(0, state.currentPage), maxPageIndex);
237
+ const clampedPageIndex = Math.min(
238
+ Math.max(0, state.currentPage),
239
+ maxPageIndex
240
+ );
211
241
  if (clampedPageIndex !== state.currentPage && visiblePages.length > 0) {
212
242
  dispatch({ type: "SET_PAGE", page: clampedPageIndex });
213
243
  }
@@ -300,7 +330,7 @@ function useForma(options) {
300
330
  const validFields = new Set(spec.fieldOrder);
301
331
  for (const fieldId of spec.fieldOrder) {
302
332
  const fieldDef = spec.fields[fieldId];
303
- if (fieldDef == null ? void 0 : fieldDef.itemFields) {
333
+ if ((fieldDef == null ? void 0 : fieldDef.type) === "array" && fieldDef.itemFields) {
304
334
  for (const key of fieldHandlers.current.keys()) {
305
335
  if (key.startsWith(`${fieldId}[`)) {
306
336
  validFields.add(key);
@@ -315,183 +345,272 @@ function useForma(options) {
315
345
  }
316
346
  }
317
347
  }, [spec]);
318
- const getFieldHandlers = useCallback((path) => {
319
- if (!fieldHandlers.current.has(path)) {
320
- fieldHandlers.current.set(path, {
321
- onChange: (value) => setValueAtPath(path, value),
322
- onBlur: () => setFieldTouched(path)
323
- });
324
- }
325
- return fieldHandlers.current.get(path);
326
- }, [setValueAtPath, setFieldTouched]);
327
- const getFieldProps = useCallback((path) => {
328
- var _a;
329
- const fieldDef = spec.fields[path];
330
- const handlers = getFieldHandlers(path);
331
- let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
332
- if (!fieldType || fieldType === "computed") {
333
- const schemaProperty2 = spec.schema.properties[path];
334
- if (schemaProperty2) {
335
- if (schemaProperty2.type === "number") fieldType = "number";
336
- else if (schemaProperty2.type === "integer") fieldType = "integer";
337
- else if (schemaProperty2.type === "boolean") fieldType = "boolean";
338
- else if (schemaProperty2.type === "array") fieldType = "array";
339
- else if (schemaProperty2.type === "object") fieldType = "object";
340
- else if ("enum" in schemaProperty2 && schemaProperty2.enum) fieldType = "select";
341
- else if ("format" in schemaProperty2) {
342
- if (schemaProperty2.format === "date") fieldType = "date";
343
- else if (schemaProperty2.format === "date-time") fieldType = "datetime";
344
- else if (schemaProperty2.format === "email") fieldType = "email";
345
- else if (schemaProperty2.format === "uri") fieldType = "url";
346
- }
348
+ const getFieldHandlers = useCallback(
349
+ (path) => {
350
+ if (!fieldHandlers.current.has(path)) {
351
+ fieldHandlers.current.set(path, {
352
+ onChange: (value) => setValueAtPath(path, value),
353
+ onBlur: () => setFieldTouched(path)
354
+ });
347
355
  }
348
- }
349
- const fieldErrors = validation.errors.filter((e) => e.field === path);
350
- const isTouched = state.touched[path] ?? false;
351
- const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
352
- const displayedErrors = showErrors ? fieldErrors : [];
353
- const hasErrors = displayedErrors.length > 0;
354
- const isRequired = required[path] ?? false;
355
- const schemaProperty = spec.schema.properties[path];
356
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
357
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
358
- const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
359
- return {
360
- name: path,
361
- value: getValueAtPath(path),
362
- type: fieldType,
363
- label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
364
- description: fieldDef == null ? void 0 : fieldDef.description,
365
- placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
366
- visible: visibility[path] !== false,
367
- enabled: enabled[path] !== false,
368
- required: isRequired,
369
- showRequiredIndicator,
370
- touched: isTouched,
371
- errors: displayedErrors,
372
- onChange: handlers.onChange,
373
- onBlur: handlers.onBlur,
374
- // ARIA accessibility attributes
375
- "aria-invalid": hasErrors || void 0,
376
- "aria-describedby": hasErrors ? `${path}-error` : void 0,
377
- "aria-required": isRequired || void 0
378
- };
379
- }, [spec, state.touched, state.isSubmitted, visibility, enabled, required, validation.errors, validateOn, getValueAtPath, getFieldHandlers]);
380
- const getSelectFieldProps = useCallback((path) => {
381
- const baseProps = getFieldProps(path);
382
- const visibleOptions = optionsVisibility[path] ?? [];
383
- return {
384
- ...baseProps,
385
- options: visibleOptions
386
- };
387
- }, [getFieldProps, optionsVisibility]);
388
- const getArrayHelpers = useCallback((path) => {
389
- const fieldDef = spec.fields[path];
390
- const currentValue = getValueAtPath(path) ?? [];
391
- const minItems = (fieldDef == null ? void 0 : fieldDef.minItems) ?? 0;
392
- const maxItems = (fieldDef == null ? void 0 : fieldDef.maxItems) ?? Infinity;
393
- const canAdd = currentValue.length < maxItems;
394
- const canRemove = currentValue.length > minItems;
395
- const getItemFieldProps = (index, fieldName) => {
356
+ return fieldHandlers.current.get(path);
357
+ },
358
+ [setValueAtPath, setFieldTouched]
359
+ );
360
+ const getFieldProps = useCallback(
361
+ (path) => {
396
362
  var _a;
397
- const itemPath = `${path}[${index}].${fieldName}`;
398
- const itemFieldDef = (_a = fieldDef == null ? void 0 : fieldDef.itemFields) == null ? void 0 : _a[fieldName];
399
- const handlers = getFieldHandlers(itemPath);
400
- const item = currentValue[index] ?? {};
401
- const itemValue = item[fieldName];
402
- const fieldErrors = validation.errors.filter((e) => e.field === itemPath);
403
- const isTouched = state.touched[itemPath] ?? false;
363
+ const fieldDef = spec.fields[path];
364
+ const handlers = getFieldHandlers(path);
365
+ let fieldType = (fieldDef == null ? void 0 : fieldDef.type) || "text";
366
+ if (!fieldType || fieldType === "computed") {
367
+ const schemaProperty2 = spec.schema.properties[path];
368
+ if (schemaProperty2) {
369
+ if (schemaProperty2.type === "number") fieldType = "number";
370
+ else if (schemaProperty2.type === "integer") fieldType = "integer";
371
+ else if (schemaProperty2.type === "boolean") fieldType = "boolean";
372
+ else if (schemaProperty2.type === "array") fieldType = "array";
373
+ else if (schemaProperty2.type === "object") fieldType = "object";
374
+ else if ("enum" in schemaProperty2 && schemaProperty2.enum)
375
+ fieldType = "select";
376
+ else if ("format" in schemaProperty2) {
377
+ if (schemaProperty2.format === "date") fieldType = "date";
378
+ else if (schemaProperty2.format === "date-time")
379
+ fieldType = "datetime";
380
+ else if (schemaProperty2.format === "email") fieldType = "email";
381
+ else if (schemaProperty2.format === "uri") fieldType = "url";
382
+ }
383
+ }
384
+ }
385
+ const fieldErrors = validation.errors.filter((e) => e.field === path);
386
+ const isTouched = state.touched[path] ?? false;
404
387
  const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
405
- const visibleOptions = optionsVisibility[itemPath];
388
+ const displayedErrors = showErrors ? fieldErrors : [];
389
+ const hasErrors = displayedErrors.length > 0;
390
+ const isRequired = required[path] ?? false;
391
+ const schemaProperty = spec.schema.properties[path];
392
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
393
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
394
+ const showRequiredIndicator = isRequired && (!isBooleanField || hasValidationRules);
395
+ const adornerProps = fieldDef && isAdornableField(fieldDef) ? { prefix: fieldDef.prefix, suffix: fieldDef.suffix } : {};
406
396
  return {
407
- name: itemPath,
408
- value: itemValue,
409
- type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
410
- label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
411
- description: itemFieldDef == null ? void 0 : itemFieldDef.description,
412
- placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
413
- visible: true,
397
+ name: path,
398
+ value: getValueAtPath(path),
399
+ type: fieldType,
400
+ label: (fieldDef == null ? void 0 : fieldDef.label) || path.charAt(0).toUpperCase() + path.slice(1),
401
+ description: fieldDef == null ? void 0 : fieldDef.description,
402
+ placeholder: fieldDef == null ? void 0 : fieldDef.placeholder,
403
+ visible: visibility[path] !== false,
414
404
  enabled: enabled[path] !== false,
415
- required: false,
416
- // TODO: Evaluate item field required
417
- showRequiredIndicator: false,
418
- // Item fields don't show required indicator
405
+ readonly: readonly[path] ?? false,
406
+ required: isRequired,
407
+ showRequiredIndicator,
419
408
  touched: isTouched,
420
- errors: showErrors ? fieldErrors : [],
409
+ errors: displayedErrors,
421
410
  onChange: handlers.onChange,
422
411
  onBlur: handlers.onBlur,
412
+ // ARIA accessibility attributes
413
+ "aria-invalid": hasErrors || void 0,
414
+ "aria-describedby": hasErrors ? `${path}-error` : void 0,
415
+ "aria-required": isRequired || void 0,
416
+ // Adorner props (only for adornable field types)
417
+ ...adornerProps,
418
+ // Presentation variant
419
+ variant: fieldDef == null ? void 0 : fieldDef.variant,
420
+ variantConfig: fieldDef == null ? void 0 : fieldDef.variantConfig
421
+ };
422
+ },
423
+ [
424
+ spec,
425
+ state.touched,
426
+ state.isSubmitted,
427
+ visibility,
428
+ enabled,
429
+ readonly,
430
+ required,
431
+ validation.errors,
432
+ validateOn,
433
+ getValueAtPath,
434
+ getFieldHandlers
435
+ ]
436
+ );
437
+ const getSelectFieldProps = useCallback(
438
+ (path) => {
439
+ const baseProps = getFieldProps(path);
440
+ const visibleOptions = optionsVisibility[path] ?? [];
441
+ return {
442
+ ...baseProps,
423
443
  options: visibleOptions
424
444
  };
425
- };
426
- return {
427
- items: currentValue,
428
- push: (item) => {
429
- if (canAdd) {
430
- setValueAtPath(path, [...currentValue, item]);
431
- }
432
- },
433
- remove: (index) => {
434
- if (canRemove) {
445
+ },
446
+ [getFieldProps, optionsVisibility]
447
+ );
448
+ const getArrayHelpers = useCallback(
449
+ (path) => {
450
+ const fieldDef = spec.fields[path];
451
+ const currentValue = getValueAtPath(path) ?? [];
452
+ const arrayDef = (fieldDef == null ? void 0 : fieldDef.type) === "array" ? fieldDef : void 0;
453
+ const minItems = (arrayDef == null ? void 0 : arrayDef.minItems) ?? 0;
454
+ const maxItems = (arrayDef == null ? void 0 : arrayDef.maxItems) ?? Infinity;
455
+ const canAdd = currentValue.length < maxItems;
456
+ const canRemove = currentValue.length > minItems;
457
+ const getItemFieldProps = (index, fieldName) => {
458
+ var _a;
459
+ const itemPath = `${path}[${index}].${fieldName}`;
460
+ const itemFieldDef = (_a = arrayDef == null ? void 0 : arrayDef.itemFields) == null ? void 0 : _a[fieldName];
461
+ const handlers = getFieldHandlers(itemPath);
462
+ const item = currentValue[index] ?? {};
463
+ const itemValue = item[fieldName];
464
+ const fieldErrors = validation.errors.filter(
465
+ (e) => e.field === itemPath
466
+ );
467
+ const isTouched = state.touched[itemPath] ?? false;
468
+ const showErrors = validateOn === "change" || validateOn === "blur" && isTouched || state.isSubmitted;
469
+ const visibleOptions = optionsVisibility[itemPath];
470
+ return {
471
+ name: itemPath,
472
+ value: itemValue,
473
+ type: (itemFieldDef == null ? void 0 : itemFieldDef.type) || "text",
474
+ label: (itemFieldDef == null ? void 0 : itemFieldDef.label) || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
475
+ description: itemFieldDef == null ? void 0 : itemFieldDef.description,
476
+ placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
477
+ visible: true,
478
+ enabled: enabled[path] !== false,
479
+ readonly: readonly[itemPath] ?? false,
480
+ required: false,
481
+ // TODO: Evaluate item field required
482
+ showRequiredIndicator: false,
483
+ // Item fields don't show required indicator
484
+ touched: isTouched,
485
+ errors: showErrors ? fieldErrors : [],
486
+ onChange: handlers.onChange,
487
+ onBlur: handlers.onBlur,
488
+ options: visibleOptions
489
+ };
490
+ };
491
+ return {
492
+ items: currentValue,
493
+ push: (item) => {
494
+ if (canAdd) {
495
+ setValueAtPath(path, [...currentValue, item]);
496
+ }
497
+ },
498
+ remove: (index) => {
499
+ if (canRemove) {
500
+ const newArray = [...currentValue];
501
+ newArray.splice(index, 1);
502
+ setValueAtPath(path, newArray);
503
+ }
504
+ },
505
+ move: (from, to) => {
435
506
  const newArray = [...currentValue];
436
- newArray.splice(index, 1);
507
+ const [item] = newArray.splice(from, 1);
508
+ newArray.splice(to, 0, item);
437
509
  setValueAtPath(path, newArray);
438
- }
439
- },
440
- move: (from, to) => {
441
- const newArray = [...currentValue];
442
- const [item] = newArray.splice(from, 1);
443
- newArray.splice(to, 0, item);
444
- setValueAtPath(path, newArray);
445
- },
446
- swap: (indexA, indexB) => {
447
- const newArray = [...currentValue];
448
- [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
449
- setValueAtPath(path, newArray);
450
- },
451
- insert: (index, item) => {
452
- if (canAdd) {
510
+ },
511
+ swap: (indexA, indexB) => {
453
512
  const newArray = [...currentValue];
454
- newArray.splice(index, 0, item);
513
+ [newArray[indexA], newArray[indexB]] = [
514
+ newArray[indexB],
515
+ newArray[indexA]
516
+ ];
455
517
  setValueAtPath(path, newArray);
456
- }
457
- },
458
- getItemFieldProps,
459
- minItems,
460
- maxItems,
461
- canAdd,
462
- canRemove
463
- };
464
- }, [spec.fields, getValueAtPath, setValueAtPath, getFieldHandlers, enabled, state.touched, state.isSubmitted, validation.errors, validateOn, optionsVisibility]);
465
- return {
466
- data: state.data,
467
- computed,
468
- visibility,
469
- required,
470
- enabled,
471
- optionsVisibility,
472
- touched: state.touched,
473
- errors: validation.errors,
474
- isValid: validation.valid,
475
- isSubmitting: state.isSubmitting,
476
- isSubmitted: state.isSubmitted,
477
- isDirty: state.isDirty,
478
- spec,
479
- wizard,
480
- setFieldValue,
481
- setFieldTouched,
482
- setValues,
483
- validateField,
484
- validateForm,
485
- submitForm,
486
- resetForm,
487
- getFieldProps,
488
- getSelectFieldProps,
489
- getArrayHelpers
490
- };
518
+ },
519
+ insert: (index, item) => {
520
+ if (canAdd) {
521
+ const newArray = [...currentValue];
522
+ newArray.splice(index, 0, item);
523
+ setValueAtPath(path, newArray);
524
+ }
525
+ },
526
+ getItemFieldProps,
527
+ minItems,
528
+ maxItems,
529
+ canAdd,
530
+ canRemove
531
+ };
532
+ },
533
+ [
534
+ spec.fields,
535
+ getValueAtPath,
536
+ setValueAtPath,
537
+ getFieldHandlers,
538
+ enabled,
539
+ readonly,
540
+ state.touched,
541
+ state.isSubmitted,
542
+ validation.errors,
543
+ validateOn,
544
+ optionsVisibility
545
+ ]
546
+ );
547
+ return useMemo(
548
+ () => ({
549
+ data: state.data,
550
+ computed,
551
+ visibility,
552
+ required,
553
+ enabled,
554
+ readonly,
555
+ optionsVisibility,
556
+ touched: state.touched,
557
+ errors: validation.errors,
558
+ isValid: validation.valid,
559
+ isSubmitting: state.isSubmitting,
560
+ isSubmitted: state.isSubmitted,
561
+ isDirty: state.isDirty,
562
+ spec,
563
+ wizard,
564
+ setFieldValue,
565
+ setFieldTouched,
566
+ setValues,
567
+ validateField,
568
+ validateForm,
569
+ submitForm,
570
+ resetForm,
571
+ getFieldProps,
572
+ getSelectFieldProps,
573
+ getArrayHelpers
574
+ }),
575
+ [
576
+ state.data,
577
+ state.touched,
578
+ state.isSubmitting,
579
+ state.isSubmitted,
580
+ state.isDirty,
581
+ computed,
582
+ visibility,
583
+ required,
584
+ enabled,
585
+ readonly,
586
+ optionsVisibility,
587
+ validation.errors,
588
+ validation.valid,
589
+ spec,
590
+ wizard,
591
+ setFieldValue,
592
+ setFieldTouched,
593
+ setValues,
594
+ validateField,
595
+ validateForm,
596
+ submitForm,
597
+ resetForm,
598
+ getFieldProps,
599
+ getSelectFieldProps,
600
+ getArrayHelpers
601
+ ]
602
+ );
491
603
  }
492
604
 
493
605
  // src/FormRenderer.tsx
494
- import React, { forwardRef, useImperativeHandle, useRef as useRef2, useMemo as useMemo2, useCallback as useCallback2 } from "react";
606
+ import React, {
607
+ forwardRef,
608
+ useImperativeHandle,
609
+ useRef as useRef2,
610
+ useMemo as useMemo2,
611
+ useCallback as useCallback2
612
+ } from "react";
613
+ import { isAdornableField as isAdornableField2, isSelectionField } from "@fogpipe/forma-core";
495
614
 
496
615
  // src/context.ts
497
616
  import { createContext, useContext } from "react";
@@ -521,7 +640,14 @@ function DefaultLayout({ children, onSubmit, isSubmitting }) {
521
640
  }
522
641
  );
523
642
  }
524
- function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }) {
643
+ function DefaultFieldWrapper({
644
+ fieldPath,
645
+ field,
646
+ children,
647
+ errors,
648
+ showRequiredIndicator,
649
+ visible
650
+ }) {
525
651
  if (!visible) return null;
526
652
  const errorId = `${fieldPath}-error`;
527
653
  const descriptionId = field.description ? `${fieldPath}-description` : void 0;
@@ -546,7 +672,11 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredI
546
672
  field.description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "field-description", children: field.description })
547
673
  ] });
548
674
  }
549
- function DefaultPageWrapper({ title, description, children }) {
675
+ function DefaultPageWrapper({
676
+ title,
677
+ description,
678
+ children
679
+ }) {
550
680
  return /* @__PURE__ */ jsxs("div", { className: "page-wrapper", children: [
551
681
  /* @__PURE__ */ jsx("h2", { children: title }),
552
682
  description && /* @__PURE__ */ jsx("p", { children: description }),
@@ -625,6 +755,20 @@ var FormRenderer = forwardRef(
625
755
  }),
626
756
  [forma, focusField, focusFirstError]
627
757
  );
758
+ const {
759
+ data: formaData,
760
+ computed: formaComputed,
761
+ visibility: formaVisibility,
762
+ required: formaRequired,
763
+ enabled: formaEnabled,
764
+ readonly: formaReadonly,
765
+ optionsVisibility: formaOptionsVisibility,
766
+ touched: formaTouched,
767
+ errors: formaErrors,
768
+ setFieldValue,
769
+ setFieldTouched,
770
+ getArrayHelpers
771
+ } = forma;
628
772
  const fieldsToRender = useMemo2(() => {
629
773
  var _a;
630
774
  if (spec.pages && spec.pages.length > 0 && forma.wizard) {
@@ -636,131 +780,179 @@ var FormRenderer = forwardRef(
636
780
  }
637
781
  return spec.fieldOrder;
638
782
  }, [spec.pages, spec.fieldOrder, forma.wizard]);
639
- const renderField = useCallback2((fieldPath) => {
640
- var _a;
641
- const fieldDef = spec.fields[fieldPath];
642
- if (!fieldDef) return null;
643
- const isVisible = forma.visibility[fieldPath] !== false;
644
- if (!isVisible) return null;
645
- const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
646
- const componentKey = fieldType;
647
- const Component = components[componentKey] || components.fallback;
648
- if (!Component) {
649
- console.warn(`No component found for field type: ${fieldType}`);
650
- return null;
651
- }
652
- const errors = forma.errors.filter((e) => e.field === fieldPath);
653
- const touched = forma.touched[fieldPath] ?? false;
654
- const required = forma.required[fieldPath] ?? false;
655
- const disabled = forma.enabled[fieldPath] === false;
656
- const schemaProperty = spec.schema.properties[fieldPath];
657
- const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
658
- const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
659
- const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
660
- const baseProps = {
661
- name: fieldPath,
662
- field: fieldDef,
663
- value: forma.data[fieldPath],
664
- touched,
665
- required,
666
- disabled,
667
- errors,
668
- onChange: (value) => forma.setFieldValue(fieldPath, value),
669
- onBlur: () => forma.setFieldTouched(fieldPath),
670
- // Convenience properties
671
- visible: true,
672
- // Always true since we already filtered for visibility
673
- enabled: !disabled,
674
- label: fieldDef.label ?? fieldPath,
675
- description: fieldDef.description,
676
- placeholder: fieldDef.placeholder
677
- };
678
- let fieldProps = baseProps;
679
- if (fieldType === "number" || fieldType === "integer") {
680
- const constraints = getNumberConstraints(schemaProperty);
681
- fieldProps = {
682
- ...baseProps,
683
- fieldType,
684
- value: baseProps.value,
685
- onChange: baseProps.onChange,
686
- ...constraints
687
- };
688
- } else if (fieldType === "select" || fieldType === "multiselect") {
689
- fieldProps = {
690
- ...baseProps,
691
- fieldType,
692
- value: baseProps.value,
693
- onChange: baseProps.onChange,
694
- options: forma.optionsVisibility[fieldPath] ?? fieldDef.options ?? []
695
- };
696
- } else if (fieldType === "array" && fieldDef.itemFields) {
697
- const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
698
- const minItems = fieldDef.minItems ?? 0;
699
- const maxItems = fieldDef.maxItems ?? Infinity;
700
- const itemFieldDefs = fieldDef.itemFields;
701
- const baseHelpers = forma.getArrayHelpers(fieldPath);
702
- const pushWithDefault = (item) => {
703
- const newItem = item ?? createDefaultItem(itemFieldDefs);
704
- baseHelpers.push(newItem);
705
- };
706
- const getItemFieldPropsExtended = (index, fieldName) => {
707
- const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
708
- const itemFieldDef = itemFieldDefs[fieldName];
709
- const itemPath = `${fieldPath}[${index}].${fieldName}`;
710
- return {
711
- ...baseProps2,
712
- itemIndex: index,
713
- fieldName,
714
- options: forma.optionsVisibility[itemPath] ?? (itemFieldDef == null ? void 0 : itemFieldDef.options)
715
- };
716
- };
717
- const helpers = {
718
- items: arrayValue,
719
- push: pushWithDefault,
720
- insert: baseHelpers.insert,
721
- remove: baseHelpers.remove,
722
- move: baseHelpers.move,
723
- swap: baseHelpers.swap,
724
- getItemFieldProps: getItemFieldPropsExtended,
725
- minItems,
726
- maxItems,
727
- canAdd: arrayValue.length < maxItems,
728
- canRemove: arrayValue.length > minItems
729
- };
730
- fieldProps = {
731
- ...baseProps,
732
- fieldType: "array",
733
- value: arrayValue,
734
- onChange: baseProps.onChange,
735
- helpers,
736
- itemFields: itemFieldDefs,
737
- minItems,
738
- maxItems
739
- };
740
- } else {
741
- fieldProps = {
742
- ...baseProps,
743
- fieldType,
744
- value: baseProps.value ?? "",
745
- onChange: baseProps.onChange
746
- };
747
- }
748
- const componentProps = { field: fieldProps, spec };
749
- return /* @__PURE__ */ jsx(
750
- FieldWrapper,
751
- {
752
- fieldPath,
783
+ const renderField = useCallback2(
784
+ (fieldPath) => {
785
+ var _a;
786
+ const fieldDef = spec.fields[fieldPath];
787
+ if (!fieldDef) return null;
788
+ const isVisible = formaVisibility[fieldPath] !== false;
789
+ if (!isVisible) {
790
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, hidden: true }, fieldPath);
791
+ }
792
+ const fieldType = fieldDef.type;
793
+ const componentKey = fieldType;
794
+ const Component = components[componentKey] || components.fallback;
795
+ if (!Component) {
796
+ console.warn(`No component found for field type: ${fieldType}`);
797
+ return null;
798
+ }
799
+ const errors = formaErrors.filter((e) => e.field === fieldPath);
800
+ const touched = formaTouched[fieldPath] ?? false;
801
+ const required = formaRequired[fieldPath] ?? false;
802
+ const disabled = formaEnabled[fieldPath] === false;
803
+ const schemaProperty = spec.schema.properties[fieldPath];
804
+ const isBooleanField = (schemaProperty == null ? void 0 : schemaProperty.type) === "boolean" || (fieldDef == null ? void 0 : fieldDef.type) === "boolean";
805
+ const hasValidationRules = (((_a = fieldDef == null ? void 0 : fieldDef.validations) == null ? void 0 : _a.length) ?? 0) > 0;
806
+ const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
807
+ const isReadonly = formaReadonly[fieldPath] ?? false;
808
+ const baseProps = {
809
+ name: fieldPath,
753
810
  field: fieldDef,
754
- errors,
811
+ value: formaData[fieldPath],
755
812
  touched,
756
813
  required,
757
- showRequiredIndicator,
758
- visible: isVisible,
759
- children: React.createElement(Component, componentProps)
760
- },
761
- fieldPath
762
- );
763
- }, [spec, forma, components, FieldWrapper]);
814
+ disabled,
815
+ errors,
816
+ onChange: (value) => setFieldValue(fieldPath, value),
817
+ onBlur: () => setFieldTouched(fieldPath),
818
+ // Convenience properties
819
+ visible: true,
820
+ // Always true since we already filtered for visibility
821
+ enabled: !disabled,
822
+ readonly: isReadonly,
823
+ label: fieldDef.label ?? fieldPath,
824
+ description: fieldDef.description,
825
+ placeholder: fieldDef.placeholder,
826
+ // Adorner properties (only for adornable field types)
827
+ ...isAdornableField2(fieldDef) && {
828
+ prefix: fieldDef.prefix,
829
+ suffix: fieldDef.suffix
830
+ },
831
+ // Presentation variant
832
+ variant: fieldDef.variant,
833
+ variantConfig: fieldDef.variantConfig
834
+ };
835
+ let fieldProps = baseProps;
836
+ if (fieldType === "number" || fieldType === "integer") {
837
+ const constraints = getNumberConstraints(schemaProperty);
838
+ fieldProps = {
839
+ ...baseProps,
840
+ fieldType,
841
+ value: baseProps.value,
842
+ onChange: baseProps.onChange,
843
+ ...constraints
844
+ };
845
+ } else if (fieldType === "select" || fieldType === "multiselect") {
846
+ const selectOptions = isSelectionField(fieldDef) ? fieldDef.options : [];
847
+ fieldProps = {
848
+ ...baseProps,
849
+ fieldType,
850
+ value: baseProps.value,
851
+ onChange: baseProps.onChange,
852
+ options: formaOptionsVisibility[fieldPath] ?? selectOptions ?? []
853
+ };
854
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
855
+ const arrayValue = Array.isArray(baseProps.value) ? baseProps.value : [];
856
+ const minItems = fieldDef.minItems ?? 0;
857
+ const maxItems = fieldDef.maxItems ?? Infinity;
858
+ const itemFieldDefs = fieldDef.itemFields;
859
+ const baseHelpers = getArrayHelpers(fieldPath);
860
+ const pushWithDefault = (item) => {
861
+ const newItem = item ?? createDefaultItem(itemFieldDefs);
862
+ baseHelpers.push(newItem);
863
+ };
864
+ const getItemFieldPropsExtended = (index, fieldName) => {
865
+ const baseProps2 = baseHelpers.getItemFieldProps(index, fieldName);
866
+ const itemFieldDef = itemFieldDefs[fieldName];
867
+ const itemPath = `${fieldPath}[${index}].${fieldName}`;
868
+ return {
869
+ ...baseProps2,
870
+ itemIndex: index,
871
+ fieldName,
872
+ options: formaOptionsVisibility[itemPath] ?? (itemFieldDef && isSelectionField(itemFieldDef) ? itemFieldDef.options : void 0)
873
+ };
874
+ };
875
+ const helpers = {
876
+ items: arrayValue,
877
+ push: pushWithDefault,
878
+ insert: baseHelpers.insert,
879
+ remove: baseHelpers.remove,
880
+ move: baseHelpers.move,
881
+ swap: baseHelpers.swap,
882
+ getItemFieldProps: getItemFieldPropsExtended,
883
+ minItems,
884
+ maxItems,
885
+ canAdd: arrayValue.length < maxItems,
886
+ canRemove: arrayValue.length > minItems
887
+ };
888
+ fieldProps = {
889
+ ...baseProps,
890
+ fieldType: "array",
891
+ value: arrayValue,
892
+ onChange: baseProps.onChange,
893
+ helpers,
894
+ itemFields: itemFieldDefs,
895
+ minItems,
896
+ maxItems
897
+ };
898
+ } else if (fieldType === "display" && fieldDef.type === "display") {
899
+ const sourceValue = fieldDef.source ? formaData[fieldDef.source] ?? formaComputed[fieldDef.source] : void 0;
900
+ const {
901
+ onChange: _onChange,
902
+ value: _value,
903
+ ...displayBaseProps
904
+ } = baseProps;
905
+ fieldProps = {
906
+ ...displayBaseProps,
907
+ fieldType: "display",
908
+ content: fieldDef.content,
909
+ sourceValue,
910
+ format: fieldDef.format
911
+ };
912
+ } else {
913
+ fieldProps = {
914
+ ...baseProps,
915
+ fieldType,
916
+ value: baseProps.value ?? "",
917
+ onChange: baseProps.onChange
918
+ };
919
+ }
920
+ const componentProps = { field: fieldProps, spec };
921
+ return /* @__PURE__ */ jsx("div", { "data-field-path": fieldPath, children: /* @__PURE__ */ jsx(
922
+ FieldWrapper,
923
+ {
924
+ fieldPath,
925
+ field: fieldDef,
926
+ errors,
927
+ touched,
928
+ required,
929
+ showRequiredIndicator,
930
+ visible: isVisible,
931
+ children: React.createElement(
932
+ Component,
933
+ componentProps
934
+ )
935
+ }
936
+ ) }, fieldPath);
937
+ },
938
+ [
939
+ spec,
940
+ components,
941
+ FieldWrapper,
942
+ formaData,
943
+ formaComputed,
944
+ formaVisibility,
945
+ formaRequired,
946
+ formaEnabled,
947
+ formaReadonly,
948
+ formaOptionsVisibility,
949
+ formaTouched,
950
+ formaErrors,
951
+ setFieldValue,
952
+ setFieldTouched,
953
+ getArrayHelpers
954
+ ]
955
+ );
764
956
  const renderedFields = useMemo2(
765
957
  () => fieldsToRender.map(renderField),
766
958
  [fieldsToRender, renderField]
@@ -796,6 +988,7 @@ var FormRenderer = forwardRef(
796
988
 
797
989
  // src/FieldRenderer.tsx
798
990
  import React2 from "react";
991
+ import { isAdornableField as isAdornableField3 } from "@fogpipe/forma-core";
799
992
  import { jsx as jsx2 } from "react/jsx-runtime";
800
993
  function getNumberConstraints2(schema) {
801
994
  if (!schema) return {};
@@ -823,7 +1016,11 @@ function createDefaultItem2(itemFields) {
823
1016
  }
824
1017
  return item;
825
1018
  }
826
- function FieldRenderer({ fieldPath, components, className }) {
1019
+ function FieldRenderer({
1020
+ fieldPath,
1021
+ components,
1022
+ className
1023
+ }) {
827
1024
  const forma = useFormaContext();
828
1025
  const { spec } = forma;
829
1026
  const fieldDef = spec.fields[fieldPath];
@@ -832,8 +1029,10 @@ function FieldRenderer({ fieldPath, components, className }) {
832
1029
  return null;
833
1030
  }
834
1031
  const isVisible = forma.visibility[fieldPath] !== false;
835
- if (!isVisible) return null;
836
- const fieldType = fieldDef.type || (fieldDef.itemFields ? "array" : "text");
1032
+ if (!isVisible) {
1033
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, hidden: true });
1034
+ }
1035
+ const fieldType = fieldDef.type;
837
1036
  const componentKey = fieldType;
838
1037
  const Component = components[componentKey] || components.fallback;
839
1038
  if (!Component) {
@@ -845,6 +1044,7 @@ function FieldRenderer({ fieldPath, components, className }) {
845
1044
  const required = forma.required[fieldPath] ?? false;
846
1045
  const disabled = forma.enabled[fieldPath] === false;
847
1046
  const schemaProperty = spec.schema.properties[fieldPath];
1047
+ const isReadonly = forma.readonly[fieldPath] ?? false;
848
1048
  const baseProps = {
849
1049
  name: fieldPath,
850
1050
  field: fieldDef,
@@ -859,9 +1059,18 @@ function FieldRenderer({ fieldPath, components, className }) {
859
1059
  visible: true,
860
1060
  // Always true since we already filtered for visibility
861
1061
  enabled: !disabled,
1062
+ readonly: isReadonly,
862
1063
  label: fieldDef.label ?? fieldPath,
863
1064
  description: fieldDef.description,
864
- placeholder: fieldDef.placeholder
1065
+ placeholder: fieldDef.placeholder,
1066
+ // Adorner properties (only for adornable field types)
1067
+ ...isAdornableField3(fieldDef) && {
1068
+ prefix: fieldDef.prefix,
1069
+ suffix: fieldDef.suffix
1070
+ },
1071
+ // Presentation variant
1072
+ variant: fieldDef.variant,
1073
+ variantConfig: fieldDef.variantConfig
865
1074
  };
866
1075
  let fieldProps = baseProps;
867
1076
  if (fieldType === "number") {
@@ -901,7 +1110,7 @@ function FieldRenderer({ fieldPath, components, className }) {
901
1110
  onChange: baseProps.onChange,
902
1111
  options: visibleOptions
903
1112
  };
904
- } else if (fieldType === "array" && fieldDef.itemFields) {
1113
+ } else if (fieldType === "array" && fieldDef.type === "array" && fieldDef.itemFields) {
905
1114
  const arrayValue = baseProps.value ?? [];
906
1115
  const minItems = fieldDef.minItems ?? 0;
907
1116
  const maxItems = fieldDef.maxItems ?? Infinity;
@@ -930,7 +1139,10 @@ function FieldRenderer({ fieldPath, components, className }) {
930
1139
  },
931
1140
  swap: (indexA, indexB) => {
932
1141
  const newArray = [...arrayValue];
933
- [newArray[indexA], newArray[indexB]] = [newArray[indexB], newArray[indexA]];
1142
+ [newArray[indexA], newArray[indexB]] = [
1143
+ newArray[indexB],
1144
+ newArray[indexA]
1145
+ ];
934
1146
  forma.setFieldValue(fieldPath, newArray);
935
1147
  },
936
1148
  getItemFieldProps: (index, fieldName) => {
@@ -948,6 +1160,7 @@ function FieldRenderer({ fieldPath, components, className }) {
948
1160
  placeholder: itemFieldDef == null ? void 0 : itemFieldDef.placeholder,
949
1161
  visible: true,
950
1162
  enabled: !disabled,
1163
+ readonly: forma.readonly[itemPath] ?? false,
951
1164
  required: (itemFieldDef == null ? void 0 : itemFieldDef.requiredWhen) === "true",
952
1165
  touched: forma.touched[itemPath] ?? false,
953
1166
  errors: forma.errors.filter((e) => e.field === itemPath),
@@ -978,6 +1191,20 @@ function FieldRenderer({ fieldPath, components, className }) {
978
1191
  minItems,
979
1192
  maxItems
980
1193
  };
1194
+ } else if (fieldType === "display" && fieldDef.type === "display") {
1195
+ const sourceValue = fieldDef.source ? forma.data[fieldDef.source] ?? forma.computed[fieldDef.source] : void 0;
1196
+ const {
1197
+ onChange: _onChange,
1198
+ value: _value,
1199
+ ...displayBaseProps
1200
+ } = baseProps;
1201
+ fieldProps = {
1202
+ ...displayBaseProps,
1203
+ fieldType: "display",
1204
+ content: fieldDef.content,
1205
+ sourceValue,
1206
+ format: fieldDef.format
1207
+ };
981
1208
  } else {
982
1209
  fieldProps = {
983
1210
  ...baseProps,
@@ -987,11 +1214,14 @@ function FieldRenderer({ fieldPath, components, className }) {
987
1214
  };
988
1215
  }
989
1216
  const componentProps = { field: fieldProps, spec };
990
- const element = React2.createElement(Component, componentProps);
1217
+ const element = React2.createElement(
1218
+ Component,
1219
+ componentProps
1220
+ );
991
1221
  if (className) {
992
- return /* @__PURE__ */ jsx2("div", { className, children: element });
1222
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, className, children: element });
993
1223
  }
994
- return element;
1224
+ return /* @__PURE__ */ jsx2("div", { "data-field-path": fieldPath, children: element });
995
1225
  }
996
1226
 
997
1227
  // src/ErrorBoundary.tsx