@fogpipe/forma-react 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +111 -26
  2. package/dist/FormRenderer-D_ZVK44t.d.ts +558 -0
  3. package/dist/chunk-5K4QITFH.js +1276 -0
  4. package/dist/chunk-5K4QITFH.js.map +1 -0
  5. package/dist/defaults/index.d.ts +56 -0
  6. package/dist/defaults/index.js +895 -0
  7. package/dist/defaults/index.js.map +1 -0
  8. package/dist/defaults/styles/forma-defaults.css +696 -0
  9. package/dist/index.d.ts +13 -549
  10. package/dist/index.js +34 -1273
  11. package/dist/index.js.map +1 -1
  12. package/package.json +17 -3
  13. package/src/FieldRenderer.tsx +12 -4
  14. package/src/FormRenderer.tsx +26 -9
  15. package/src/__tests__/FieldRenderer.test.tsx +5 -1
  16. package/src/__tests__/FormRenderer.test.tsx +146 -0
  17. package/src/__tests__/canProceed.test.ts +243 -0
  18. package/src/__tests__/defaults/components.test.tsx +818 -0
  19. package/src/__tests__/defaults/integration.test.tsx +494 -0
  20. package/src/__tests__/defaults/layout.test.tsx +298 -0
  21. package/src/__tests__/events.test.ts +15 -5
  22. package/src/__tests__/useForma.test.ts +108 -5
  23. package/src/defaults/DefaultFormRenderer.tsx +43 -0
  24. package/src/defaults/componentMap.ts +45 -0
  25. package/src/defaults/components/ArrayField.tsx +183 -0
  26. package/src/defaults/components/BooleanInput.tsx +32 -0
  27. package/src/defaults/components/ComputedDisplay.tsx +26 -0
  28. package/src/defaults/components/DateInput.tsx +59 -0
  29. package/src/defaults/components/DisplayField.tsx +15 -0
  30. package/src/defaults/components/FallbackField.tsx +35 -0
  31. package/src/defaults/components/MatrixField.tsx +98 -0
  32. package/src/defaults/components/MultiSelectInput.tsx +51 -0
  33. package/src/defaults/components/NumberInput.tsx +73 -0
  34. package/src/defaults/components/ObjectField.tsx +22 -0
  35. package/src/defaults/components/SelectInput.tsx +44 -0
  36. package/src/defaults/components/TextInput.tsx +48 -0
  37. package/src/defaults/components/TextareaInput.tsx +46 -0
  38. package/src/defaults/index.ts +33 -0
  39. package/src/defaults/layout/FieldWrapper.tsx +83 -0
  40. package/src/defaults/layout/FormLayout.tsx +34 -0
  41. package/src/defaults/layout/PageWrapper.tsx +18 -0
  42. package/src/defaults/layout/WizardLayout.tsx +130 -0
  43. package/src/defaults/styles/forma-defaults.css +696 -0
  44. package/src/events.ts +4 -1
  45. package/src/types.ts +16 -4
  46. package/src/useForma.ts +48 -34
@@ -0,0 +1,895 @@
1
+ import {
2
+ FormRenderer,
3
+ useFormaContext
4
+ } from "../chunk-5K4QITFH.js";
5
+
6
+ // src/defaults/DefaultFormRenderer.tsx
7
+ import { forwardRef } from "react";
8
+
9
+ // src/defaults/components/TextInput.tsx
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ function TextInput({ field }) {
12
+ const inputType = field.fieldType === "phone" ? "tel" : field.fieldType;
13
+ const hasErrors = field.visibleErrors.length > 0;
14
+ const describedBy = [
15
+ field.description ? `${field.name}-description` : null,
16
+ hasErrors ? `${field.name}-errors` : null
17
+ ].filter(Boolean).join(" ");
18
+ const input = /* @__PURE__ */ jsx(
19
+ "input",
20
+ {
21
+ id: field.name,
22
+ name: field.name,
23
+ type: inputType,
24
+ className: "forma-input",
25
+ value: field.value,
26
+ onChange: (e) => field.onChange(e.target.value),
27
+ onBlur: field.onBlur,
28
+ disabled: field.disabled,
29
+ readOnly: field.readonly,
30
+ placeholder: field.placeholder,
31
+ "aria-invalid": hasErrors || void 0,
32
+ "aria-required": field.required || void 0,
33
+ "aria-describedby": describedBy || void 0
34
+ }
35
+ );
36
+ if (field.prefix || field.suffix) {
37
+ return /* @__PURE__ */ jsxs("div", { className: "forma-input-adorner", children: [
38
+ field.prefix && /* @__PURE__ */ jsx("span", { className: "forma-input-adorner__prefix", children: field.prefix }),
39
+ input,
40
+ field.suffix && /* @__PURE__ */ jsx("span", { className: "forma-input-adorner__suffix", children: field.suffix })
41
+ ] });
42
+ }
43
+ return input;
44
+ }
45
+
46
+ // src/defaults/components/TextareaInput.tsx
47
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
48
+ function TextareaInput({ field }) {
49
+ const hasErrors = field.visibleErrors.length > 0;
50
+ const describedBy = [
51
+ field.description ? `${field.name}-description` : null,
52
+ hasErrors ? `${field.name}-errors` : null
53
+ ].filter(Boolean).join(" ");
54
+ const textarea = /* @__PURE__ */ jsx2(
55
+ "textarea",
56
+ {
57
+ id: field.name,
58
+ name: field.name,
59
+ className: "forma-textarea",
60
+ value: field.value,
61
+ onChange: (e) => field.onChange(e.target.value),
62
+ onBlur: field.onBlur,
63
+ disabled: field.disabled,
64
+ readOnly: field.readonly,
65
+ placeholder: field.placeholder,
66
+ rows: 3,
67
+ "aria-invalid": hasErrors || void 0,
68
+ "aria-required": field.required || void 0,
69
+ "aria-describedby": describedBy || void 0
70
+ }
71
+ );
72
+ if (field.prefix || field.suffix) {
73
+ return /* @__PURE__ */ jsxs2("div", { className: "forma-input-adorner", children: [
74
+ field.prefix && /* @__PURE__ */ jsx2("span", { className: "forma-input-adorner__prefix", children: field.prefix }),
75
+ textarea,
76
+ field.suffix && /* @__PURE__ */ jsx2("span", { className: "forma-input-adorner__suffix", children: field.suffix })
77
+ ] });
78
+ }
79
+ return textarea;
80
+ }
81
+
82
+ // src/defaults/components/NumberInput.tsx
83
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
84
+ function NumberInputBase({
85
+ field,
86
+ parseValue
87
+ }) {
88
+ const hasErrors = field.visibleErrors.length > 0;
89
+ const describedBy = [
90
+ field.description ? `${field.name}-description` : null,
91
+ hasErrors ? `${field.name}-errors` : null
92
+ ].filter(Boolean).join(" ");
93
+ const input = /* @__PURE__ */ jsx3(
94
+ "input",
95
+ {
96
+ id: field.name,
97
+ name: field.name,
98
+ type: "number",
99
+ className: `forma-input forma-input--${field.fieldType}`,
100
+ value: field.value != null ? String(field.value) : "",
101
+ onChange: (e) => {
102
+ const val = e.target.value;
103
+ if (val === "") {
104
+ field.onChange(null);
105
+ } else {
106
+ const num = parseValue(val);
107
+ field.onChange(isNaN(num) ? null : num);
108
+ }
109
+ },
110
+ onBlur: field.onBlur,
111
+ disabled: field.disabled,
112
+ readOnly: field.readonly,
113
+ placeholder: field.placeholder,
114
+ min: field.min,
115
+ max: field.max,
116
+ step: field.step ?? (field.fieldType === "integer" ? 1 : "any"),
117
+ "aria-invalid": hasErrors || void 0,
118
+ "aria-required": field.required || void 0,
119
+ "aria-describedby": describedBy || void 0
120
+ }
121
+ );
122
+ if (field.prefix || field.suffix) {
123
+ return /* @__PURE__ */ jsxs3("div", { className: "forma-input-adorner", children: [
124
+ field.prefix && /* @__PURE__ */ jsx3("span", { className: "forma-input-adorner__prefix", children: field.prefix }),
125
+ input,
126
+ field.suffix && /* @__PURE__ */ jsx3("span", { className: "forma-input-adorner__suffix", children: field.suffix })
127
+ ] });
128
+ }
129
+ return input;
130
+ }
131
+ function NumberInput({ field }) {
132
+ return /* @__PURE__ */ jsx3(NumberInputBase, { field, parseValue: parseFloat });
133
+ }
134
+ function IntegerInput({ field }) {
135
+ return /* @__PURE__ */ jsx3(NumberInputBase, { field, parseValue: (v) => parseInt(v, 10) });
136
+ }
137
+
138
+ // src/defaults/components/BooleanInput.tsx
139
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
140
+ function BooleanInput({ field }) {
141
+ const hasErrors = field.visibleErrors.length > 0;
142
+ const describedBy = [
143
+ field.description ? `${field.name}-description` : null,
144
+ hasErrors ? `${field.name}-errors` : null
145
+ ].filter(Boolean).join(" ");
146
+ return /* @__PURE__ */ jsxs4("div", { className: "forma-checkbox", children: [
147
+ /* @__PURE__ */ jsx4(
148
+ "input",
149
+ {
150
+ id: field.name,
151
+ name: field.name,
152
+ type: "checkbox",
153
+ className: "forma-checkbox__input",
154
+ checked: field.value ?? false,
155
+ onChange: (e) => field.onChange(e.target.checked),
156
+ onBlur: field.onBlur,
157
+ disabled: field.disabled,
158
+ "aria-invalid": hasErrors || void 0,
159
+ "aria-describedby": describedBy || void 0
160
+ }
161
+ ),
162
+ /* @__PURE__ */ jsx4("label", { htmlFor: field.name, className: "forma-checkbox__label", children: field.label })
163
+ ] });
164
+ }
165
+
166
+ // src/defaults/components/DateInput.tsx
167
+ import { jsx as jsx5 } from "react/jsx-runtime";
168
+ function DateInput({ field }) {
169
+ const hasErrors = field.visibleErrors.length > 0;
170
+ const describedBy = [
171
+ field.description ? `${field.name}-description` : null,
172
+ hasErrors ? `${field.name}-errors` : null
173
+ ].filter(Boolean).join(" ");
174
+ return /* @__PURE__ */ jsx5(
175
+ "input",
176
+ {
177
+ id: field.name,
178
+ name: field.name,
179
+ type: "date",
180
+ className: "forma-input forma-input--date",
181
+ value: field.value ?? "",
182
+ onChange: (e) => field.onChange(e.target.value || null),
183
+ onBlur: field.onBlur,
184
+ disabled: field.disabled,
185
+ readOnly: field.readonly,
186
+ "aria-invalid": hasErrors || void 0,
187
+ "aria-required": field.required || void 0,
188
+ "aria-describedby": describedBy || void 0
189
+ }
190
+ );
191
+ }
192
+ function DateTimeInput({ field }) {
193
+ const hasErrors = field.visibleErrors.length > 0;
194
+ const describedBy = [
195
+ field.description ? `${field.name}-description` : null,
196
+ hasErrors ? `${field.name}-errors` : null
197
+ ].filter(Boolean).join(" ");
198
+ const inputValue = field.value ?? "";
199
+ return /* @__PURE__ */ jsx5(
200
+ "input",
201
+ {
202
+ id: field.name,
203
+ name: field.name,
204
+ type: "datetime-local",
205
+ className: "forma-input forma-input--datetime",
206
+ value: inputValue,
207
+ onChange: (e) => field.onChange(e.target.value || null),
208
+ onBlur: field.onBlur,
209
+ disabled: field.disabled,
210
+ readOnly: field.readonly,
211
+ "aria-invalid": hasErrors || void 0,
212
+ "aria-required": field.required || void 0,
213
+ "aria-describedby": describedBy || void 0
214
+ }
215
+ );
216
+ }
217
+
218
+ // src/defaults/components/SelectInput.tsx
219
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
220
+ function SelectInput({ field }) {
221
+ const hasErrors = field.visibleErrors.length > 0;
222
+ const describedBy = [
223
+ field.description ? `${field.name}-description` : null,
224
+ hasErrors ? `${field.name}-errors` : null
225
+ ].filter(Boolean).join(" ");
226
+ return /* @__PURE__ */ jsxs5(
227
+ "select",
228
+ {
229
+ id: field.name,
230
+ name: field.name,
231
+ className: "forma-select",
232
+ value: field.value !== null ? String(field.value) : "",
233
+ onChange: (e) => {
234
+ const value = e.target.value;
235
+ if (!value) {
236
+ field.onChange(null);
237
+ } else {
238
+ field.onChange(value);
239
+ }
240
+ },
241
+ onBlur: field.onBlur,
242
+ disabled: field.disabled,
243
+ "aria-invalid": hasErrors || void 0,
244
+ "aria-required": field.required || void 0,
245
+ "aria-describedby": describedBy || void 0,
246
+ children: [
247
+ (!field.required || field.value === null) && /* @__PURE__ */ jsx6("option", { value: "", children: field.placeholder ?? "Select..." }),
248
+ field.options.map((opt) => /* @__PURE__ */ jsx6("option", { value: String(opt.value), children: opt.label }, String(opt.value)))
249
+ ]
250
+ }
251
+ );
252
+ }
253
+
254
+ // src/defaults/components/MultiSelectInput.tsx
255
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
256
+ function MultiSelectInput({ field }) {
257
+ const hasErrors = field.visibleErrors.length > 0;
258
+ const describedBy = [
259
+ field.description ? `${field.name}-description` : null,
260
+ hasErrors ? `${field.name}-errors` : null
261
+ ].filter(Boolean).join(" ");
262
+ const selected = field.value ?? [];
263
+ const handleToggle = (optionValue) => {
264
+ if (selected.includes(optionValue)) {
265
+ field.onChange(selected.filter((v) => v !== optionValue));
266
+ } else {
267
+ field.onChange([...selected, optionValue]);
268
+ }
269
+ field.onBlur();
270
+ };
271
+ return /* @__PURE__ */ jsxs6(
272
+ "fieldset",
273
+ {
274
+ className: "forma-multiselect",
275
+ "aria-describedby": describedBy || void 0,
276
+ "aria-invalid": hasErrors || void 0,
277
+ children: [
278
+ /* @__PURE__ */ jsx7("legend", { className: "forma-sr-only", children: field.label }),
279
+ field.options.map((opt) => {
280
+ const optId = `${field.name}-${opt.value}`;
281
+ return /* @__PURE__ */ jsxs6("div", { className: "forma-multiselect__option", children: [
282
+ /* @__PURE__ */ jsx7(
283
+ "input",
284
+ {
285
+ id: optId,
286
+ type: "checkbox",
287
+ className: "forma-checkbox__input",
288
+ checked: selected.includes(String(opt.value)),
289
+ onChange: () => handleToggle(String(opt.value)),
290
+ disabled: field.disabled
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsx7("label", { htmlFor: optId, className: "forma-checkbox__label", children: opt.label })
294
+ ] }, String(opt.value));
295
+ })
296
+ ]
297
+ }
298
+ );
299
+ }
300
+
301
+ // src/defaults/components/ArrayField.tsx
302
+ import { useRef } from "react";
303
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
304
+ function ArrayField({ field }) {
305
+ const hasErrors = field.visibleErrors.length > 0;
306
+ const itemKeysRef = useRef([]);
307
+ const nextKeyRef = useRef(0);
308
+ const currentLength = field.helpers.items.length;
309
+ const keysLength = itemKeysRef.current.length;
310
+ if (currentLength > keysLength) {
311
+ for (let i = keysLength; i < currentLength; i++) {
312
+ itemKeysRef.current.push(`item-${nextKeyRef.current++}`);
313
+ }
314
+ } else if (currentLength < keysLength) {
315
+ itemKeysRef.current.length = currentLength;
316
+ }
317
+ const fieldOrder = field.itemFieldOrder ?? Object.keys(field.itemFields);
318
+ return /* @__PURE__ */ jsxs7(
319
+ "div",
320
+ {
321
+ className: "forma-array",
322
+ "aria-invalid": hasErrors || void 0,
323
+ children: [
324
+ field.helpers.items.length === 0 && /* @__PURE__ */ jsx8("p", { className: "forma-array__empty", children: "No items" }),
325
+ field.helpers.items.map((_, index) => /* @__PURE__ */ jsxs7(
326
+ "div",
327
+ {
328
+ className: "forma-array__item",
329
+ children: [
330
+ /* @__PURE__ */ jsx8("div", { className: "forma-array__item-fields", children: fieldOrder.map((fieldName) => {
331
+ const itemProps = field.helpers.getItemFieldProps(
332
+ index,
333
+ fieldName
334
+ );
335
+ return /* @__PURE__ */ jsxs7("div", { className: "forma-field", children: [
336
+ /* @__PURE__ */ jsx8(
337
+ "label",
338
+ {
339
+ htmlFor: itemProps.name,
340
+ className: "forma-label",
341
+ children: itemProps.label
342
+ }
343
+ ),
344
+ renderItemField(itemProps),
345
+ itemProps.errors.length > 0 && itemProps.touched && /* @__PURE__ */ jsx8("div", { className: "forma-field__errors", role: "alert", children: itemProps.errors.map((err, i) => /* @__PURE__ */ jsx8("span", { className: "forma-field__error", children: err.message }, i)) })
346
+ ] }, fieldName);
347
+ }) }),
348
+ /* @__PURE__ */ jsx8(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ className: "forma-button forma-button--danger forma-array__remove",
353
+ onClick: () => field.helpers.remove(index),
354
+ disabled: !field.helpers.canRemove || field.disabled,
355
+ children: "Remove"
356
+ }
357
+ )
358
+ ]
359
+ },
360
+ itemKeysRef.current[index]
361
+ )),
362
+ /* @__PURE__ */ jsx8(
363
+ "button",
364
+ {
365
+ type: "button",
366
+ className: "forma-button forma-button--secondary forma-array__add",
367
+ onClick: () => field.helpers.push(),
368
+ disabled: !field.helpers.canAdd || field.disabled,
369
+ children: "+ Add Item"
370
+ }
371
+ )
372
+ ]
373
+ }
374
+ );
375
+ }
376
+ function renderItemField(itemProps) {
377
+ const type = itemProps.type;
378
+ if (type === "select" && itemProps.options) {
379
+ return /* @__PURE__ */ jsxs7(
380
+ "select",
381
+ {
382
+ id: itemProps.name,
383
+ className: "forma-select",
384
+ value: String(itemProps.value ?? ""),
385
+ onChange: (e) => {
386
+ var _a;
387
+ const value = e.target.value;
388
+ if (!value) {
389
+ itemProps.onChange(null);
390
+ } else {
391
+ const option = (_a = itemProps.options) == null ? void 0 : _a.find(
392
+ (opt) => String(opt.value) === value
393
+ );
394
+ itemProps.onChange(option ? option.value : value);
395
+ }
396
+ },
397
+ onBlur: itemProps.onBlur,
398
+ disabled: !itemProps.enabled,
399
+ children: [
400
+ /* @__PURE__ */ jsx8("option", { value: "", children: "Select..." }),
401
+ itemProps.options.map((opt) => /* @__PURE__ */ jsx8("option", { value: String(opt.value), children: opt.label }, String(opt.value)))
402
+ ]
403
+ }
404
+ );
405
+ }
406
+ if (type === "number" || type === "integer") {
407
+ return /* @__PURE__ */ jsx8(
408
+ "input",
409
+ {
410
+ id: itemProps.name,
411
+ type: "number",
412
+ className: "forma-input",
413
+ value: itemProps.value != null ? String(itemProps.value) : "",
414
+ onChange: (e) => {
415
+ const val = e.target.value;
416
+ if (val === "") {
417
+ itemProps.onChange(null);
418
+ } else {
419
+ const num = type === "integer" ? parseInt(val, 10) : parseFloat(val);
420
+ itemProps.onChange(isNaN(num) ? null : num);
421
+ }
422
+ },
423
+ onBlur: itemProps.onBlur,
424
+ disabled: !itemProps.enabled,
425
+ step: type === "integer" ? 1 : "any"
426
+ }
427
+ );
428
+ }
429
+ if (type === "boolean") {
430
+ return /* @__PURE__ */ jsxs7("div", { className: "forma-checkbox", children: [
431
+ /* @__PURE__ */ jsx8(
432
+ "input",
433
+ {
434
+ id: itemProps.name,
435
+ type: "checkbox",
436
+ className: "forma-checkbox__input",
437
+ checked: Boolean(itemProps.value),
438
+ onChange: (e) => itemProps.onChange(e.target.checked),
439
+ onBlur: itemProps.onBlur,
440
+ disabled: !itemProps.enabled
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsx8("label", { htmlFor: itemProps.name, className: "forma-checkbox__label", children: itemProps.label })
444
+ ] });
445
+ }
446
+ return /* @__PURE__ */ jsx8(
447
+ "input",
448
+ {
449
+ id: itemProps.name,
450
+ type: "text",
451
+ className: "forma-input",
452
+ value: String(itemProps.value ?? ""),
453
+ onChange: (e) => itemProps.onChange(e.target.value),
454
+ onBlur: itemProps.onBlur,
455
+ disabled: !itemProps.enabled,
456
+ placeholder: itemProps.placeholder
457
+ }
458
+ );
459
+ }
460
+
461
+ // src/defaults/components/ObjectField.tsx
462
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
463
+ function ObjectField({ field }) {
464
+ const hasErrors = field.visibleErrors.length > 0;
465
+ return /* @__PURE__ */ jsxs8(
466
+ "fieldset",
467
+ {
468
+ className: "forma-object",
469
+ "aria-invalid": hasErrors || void 0,
470
+ children: [
471
+ /* @__PURE__ */ jsx9("legend", { className: "forma-object__legend", children: field.label }),
472
+ field.description && /* @__PURE__ */ jsx9("p", { className: "forma-object__description", children: field.description }),
473
+ /* @__PURE__ */ jsx9("div", { className: "forma-object__fields" })
474
+ ]
475
+ }
476
+ );
477
+ }
478
+
479
+ // src/defaults/components/ComputedDisplay.tsx
480
+ import { jsx as jsx10 } from "react/jsx-runtime";
481
+ function ComputedDisplay({ field }) {
482
+ let displayValue;
483
+ if (field.value === null || field.value === void 0) {
484
+ displayValue = "\u2014";
485
+ } else if (typeof field.value === "object") {
486
+ try {
487
+ displayValue = JSON.stringify(field.value);
488
+ } catch {
489
+ displayValue = String(field.value);
490
+ }
491
+ } else {
492
+ displayValue = String(field.value);
493
+ }
494
+ return /* @__PURE__ */ jsx10(
495
+ "output",
496
+ {
497
+ id: field.name,
498
+ className: "forma-computed",
499
+ children: displayValue
500
+ }
501
+ );
502
+ }
503
+
504
+ // src/defaults/components/DisplayField.tsx
505
+ import { jsx as jsx11 } from "react/jsx-runtime";
506
+ function DisplayField({ field }) {
507
+ const content = field.sourceValue !== void 0 ? String(field.sourceValue) : field.content;
508
+ return /* @__PURE__ */ jsx11("div", { className: "forma-display", children: content && /* @__PURE__ */ jsx11("p", { className: "forma-display__content", children: content }) });
509
+ }
510
+
511
+ // src/defaults/components/MatrixField.tsx
512
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
513
+ function MatrixField({ field }) {
514
+ const hasErrors = field.visibleErrors.length > 0;
515
+ const visibleRows = field.rows.filter((r) => r.visible);
516
+ const currentValue = field.value ?? {};
517
+ const handleChange = (rowId, colValue) => {
518
+ if (field.disabled) return;
519
+ const next = { ...currentValue };
520
+ if (field.multiSelect) {
521
+ const current = currentValue[rowId] ?? [];
522
+ const colStr = String(colValue);
523
+ const exists = current.includes(colStr);
524
+ next[rowId] = exists ? current.filter((v) => v !== colStr) : [...current, colStr];
525
+ } else {
526
+ next[rowId] = colValue;
527
+ }
528
+ field.onChange(next);
529
+ field.onBlur();
530
+ };
531
+ const isChecked = (rowId, colValue) => {
532
+ const rowValue = currentValue[rowId];
533
+ if (rowValue === void 0 || rowValue === null) return false;
534
+ if (Array.isArray(rowValue)) {
535
+ return rowValue.includes(colValue);
536
+ }
537
+ return rowValue === colValue;
538
+ };
539
+ return /* @__PURE__ */ jsx12("div", { className: "forma-matrix", "aria-invalid": hasErrors || void 0, children: /* @__PURE__ */ jsxs9("table", { className: "forma-matrix__table", role: "grid", children: [
540
+ /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
541
+ /* @__PURE__ */ jsx12("th", { scope: "col", className: "forma-matrix__corner" }),
542
+ field.columns.map((col) => /* @__PURE__ */ jsx12(
543
+ "th",
544
+ {
545
+ scope: "col",
546
+ className: "forma-matrix__col-header",
547
+ children: col.label
548
+ },
549
+ String(col.value)
550
+ ))
551
+ ] }) }),
552
+ /* @__PURE__ */ jsx12("tbody", { children: visibleRows.map((row) => /* @__PURE__ */ jsxs9("tr", { className: "forma-matrix__row", children: [
553
+ /* @__PURE__ */ jsx12("th", { scope: "row", className: "forma-matrix__row-header", children: row.label }),
554
+ field.columns.map((col) => {
555
+ const cellId = `${field.name}-${row.id}-${col.value}`;
556
+ return /* @__PURE__ */ jsxs9("td", { className: "forma-matrix__cell", children: [
557
+ /* @__PURE__ */ jsx12(
558
+ "input",
559
+ {
560
+ id: cellId,
561
+ type: field.multiSelect ? "checkbox" : "radio",
562
+ name: field.multiSelect ? cellId : `${field.name}-${row.id}`,
563
+ className: field.multiSelect ? "forma-checkbox__input" : "forma-radio__input",
564
+ checked: isChecked(row.id, col.value),
565
+ onChange: () => handleChange(row.id, col.value),
566
+ disabled: field.disabled
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsxs9("label", { htmlFor: cellId, className: "forma-sr-only", children: [
570
+ row.label,
571
+ ": ",
572
+ col.label
573
+ ] })
574
+ ] }, String(col.value));
575
+ })
576
+ ] }, row.id)) })
577
+ ] }) });
578
+ }
579
+
580
+ // src/defaults/components/FallbackField.tsx
581
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
582
+ function FallbackField({ field }) {
583
+ const hasErrors = field.visibleErrors.length > 0;
584
+ const isDev = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
585
+ return /* @__PURE__ */ jsxs10("div", { className: "forma-fallback", children: [
586
+ isDev && /* @__PURE__ */ jsxs10("p", { className: "forma-fallback__warning", children: [
587
+ 'Unknown field type: "',
588
+ field.field.type,
589
+ '"'
590
+ ] }),
591
+ /* @__PURE__ */ jsx13(
592
+ "input",
593
+ {
594
+ id: field.name,
595
+ name: field.name,
596
+ type: "text",
597
+ className: "forma-input",
598
+ value: String(field.value ?? ""),
599
+ onChange: (e) => {
600
+ if ("onChange" in field && typeof field.onChange === "function") {
601
+ field.onChange(e.target.value);
602
+ }
603
+ },
604
+ onBlur: field.onBlur,
605
+ disabled: field.disabled,
606
+ "aria-invalid": hasErrors || void 0
607
+ }
608
+ )
609
+ ] });
610
+ }
611
+
612
+ // src/defaults/layout/FieldWrapper.tsx
613
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
614
+ function FieldWrapper({
615
+ fieldPath,
616
+ field,
617
+ children,
618
+ errors,
619
+ touched,
620
+ showRequiredIndicator,
621
+ visible
622
+ }) {
623
+ const { isSubmitted } = useFormaContext();
624
+ if (!visible) return null;
625
+ const shouldShowMessages = touched || isSubmitted;
626
+ const visibleErrors = shouldShowMessages ? errors.filter((e) => e.severity === "error") : [];
627
+ const visibleWarnings = shouldShowMessages ? errors.filter((e) => e.severity === "warning") : [];
628
+ const hasErrors = visibleErrors.length > 0;
629
+ const hasWarnings = visibleWarnings.length > 0;
630
+ const classNames = [
631
+ "forma-field",
632
+ hasErrors && "forma-field--error",
633
+ !hasErrors && hasWarnings && "forma-field--warning",
634
+ showRequiredIndicator && "forma-field--required"
635
+ ].filter(Boolean).join(" ");
636
+ return /* @__PURE__ */ jsxs11("div", { className: classNames, "data-field-path": fieldPath, children: [
637
+ field.label && field.type !== "boolean" && /* @__PURE__ */ jsxs11("label", { htmlFor: fieldPath, className: "forma-label", children: [
638
+ field.label,
639
+ showRequiredIndicator && /* @__PURE__ */ jsxs11("span", { className: "forma-label__required", "aria-hidden": "true", children: [
640
+ " ",
641
+ "*"
642
+ ] })
643
+ ] }),
644
+ field.description && /* @__PURE__ */ jsx14(
645
+ "div",
646
+ {
647
+ id: `${fieldPath}-description`,
648
+ className: "forma-field__description",
649
+ children: field.description
650
+ }
651
+ ),
652
+ children,
653
+ hasErrors && /* @__PURE__ */ jsx14(
654
+ "div",
655
+ {
656
+ id: `${fieldPath}-errors`,
657
+ className: "forma-field__errors",
658
+ role: "alert",
659
+ children: visibleErrors.map((error, i) => /* @__PURE__ */ jsx14("span", { className: "forma-field__error", children: error.message }, i))
660
+ }
661
+ ),
662
+ hasWarnings && /* @__PURE__ */ jsx14("div", { className: "forma-field__warnings", children: visibleWarnings.map((warning, i) => /* @__PURE__ */ jsx14("span", { className: "forma-field__warning", children: warning.message }, i)) })
663
+ ] });
664
+ }
665
+
666
+ // src/defaults/layout/FormLayout.tsx
667
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
668
+ function FormLayout({
669
+ children,
670
+ onSubmit,
671
+ isSubmitting
672
+ }) {
673
+ return /* @__PURE__ */ jsxs12(
674
+ "form",
675
+ {
676
+ className: "forma-form",
677
+ onSubmit: (e) => {
678
+ e.preventDefault();
679
+ onSubmit();
680
+ },
681
+ noValidate: true,
682
+ children: [
683
+ children,
684
+ /* @__PURE__ */ jsx15("div", { className: "forma-form__actions", children: /* @__PURE__ */ jsx15(
685
+ "button",
686
+ {
687
+ type: "submit",
688
+ className: `forma-button forma-button--primary forma-submit${isSubmitting ? " forma-submit--loading" : ""}`,
689
+ disabled: isSubmitting,
690
+ "aria-busy": isSubmitting || void 0,
691
+ children: isSubmitting ? "Submitting..." : "Submit"
692
+ }
693
+ ) })
694
+ ]
695
+ }
696
+ );
697
+ }
698
+
699
+ // src/defaults/layout/WizardLayout.tsx
700
+ import { useCallback } from "react";
701
+ import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
702
+ function WizardLayout({
703
+ children,
704
+ onSubmit,
705
+ isSubmitting
706
+ }) {
707
+ const { wizard } = useFormaContext();
708
+ const handleNext = useCallback(() => {
709
+ if (!wizard) return;
710
+ wizard.touchCurrentPageFields();
711
+ if (wizard.validateCurrentPage()) {
712
+ wizard.nextPage();
713
+ }
714
+ }, [wizard]);
715
+ const handleSubmit = useCallback(
716
+ (e) => {
717
+ e.preventDefault();
718
+ if (!wizard) {
719
+ onSubmit();
720
+ return;
721
+ }
722
+ const nativeEvent = e.nativeEvent;
723
+ const submitter = nativeEvent.submitter;
724
+ if (wizard.isLastPage && (submitter == null ? void 0 : submitter.dataset.action) === "submit") {
725
+ onSubmit();
726
+ } else if (!wizard.isLastPage) {
727
+ handleNext();
728
+ }
729
+ },
730
+ [wizard, onSubmit, handleNext]
731
+ );
732
+ if (!wizard) {
733
+ return /* @__PURE__ */ jsxs13(
734
+ "form",
735
+ {
736
+ className: "forma-form",
737
+ onSubmit: handleSubmit,
738
+ noValidate: true,
739
+ children: [
740
+ children,
741
+ /* @__PURE__ */ jsx16("div", { className: "forma-form__actions", children: /* @__PURE__ */ jsx16(
742
+ "button",
743
+ {
744
+ type: "submit",
745
+ className: "forma-button forma-button--primary forma-submit",
746
+ disabled: isSubmitting,
747
+ "aria-busy": isSubmitting || void 0,
748
+ children: isSubmitting ? "Submitting..." : "Submit"
749
+ }
750
+ ) })
751
+ ]
752
+ }
753
+ );
754
+ }
755
+ return /* @__PURE__ */ jsxs13("form", { className: "forma-wizard", onSubmit: handleSubmit, noValidate: true, children: [
756
+ /* @__PURE__ */ jsx16("div", { className: "forma-wizard__steps", role: "navigation", "aria-label": "Form progress", children: wizard.pages.map((page, index) => {
757
+ const isCompleted = index < wizard.currentPageIndex;
758
+ const isCurrent = index === wizard.currentPageIndex;
759
+ const stepClass = [
760
+ "forma-step",
761
+ isCompleted && "forma-step--completed",
762
+ isCurrent && "forma-step--current",
763
+ !isCompleted && !isCurrent && "forma-step--upcoming"
764
+ ].filter(Boolean).join(" ");
765
+ return /* @__PURE__ */ jsxs13("div", { className: stepClass, "aria-current": isCurrent ? "step" : void 0, children: [
766
+ /* @__PURE__ */ jsx16("span", { className: "forma-step__indicator", children: isCompleted ? "\u2713" : index + 1 }),
767
+ /* @__PURE__ */ jsx16("span", { className: "forma-step__label", children: page.title })
768
+ ] }, page.id);
769
+ }) }),
770
+ children,
771
+ /* @__PURE__ */ jsxs13("div", { className: "forma-wizard__nav", children: [
772
+ wizard.hasPreviousPage ? /* @__PURE__ */ jsx16(
773
+ "button",
774
+ {
775
+ type: "button",
776
+ className: "forma-button forma-button--secondary",
777
+ onClick: () => wizard.previousPage(),
778
+ children: "Previous"
779
+ }
780
+ ) : /* @__PURE__ */ jsx16("div", {}),
781
+ wizard.isLastPage ? /* @__PURE__ */ jsx16(
782
+ "button",
783
+ {
784
+ type: "submit",
785
+ "data-action": "submit",
786
+ className: `forma-button forma-button--primary forma-submit${isSubmitting ? " forma-submit--loading" : ""}`,
787
+ disabled: isSubmitting,
788
+ "aria-busy": isSubmitting || void 0,
789
+ children: isSubmitting ? "Submitting..." : "Submit"
790
+ }
791
+ ) : /* @__PURE__ */ jsx16(
792
+ "button",
793
+ {
794
+ type: "button",
795
+ className: "forma-button forma-button--primary",
796
+ onClick: handleNext,
797
+ children: "Next"
798
+ }
799
+ )
800
+ ] })
801
+ ] });
802
+ }
803
+
804
+ // src/defaults/layout/PageWrapper.tsx
805
+ import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
806
+ function PageWrapper({
807
+ title,
808
+ description,
809
+ children
810
+ }) {
811
+ return /* @__PURE__ */ jsxs14("div", { className: "forma-page", children: [
812
+ title && /* @__PURE__ */ jsx17("h2", { className: "forma-page__title", children: title }),
813
+ description && /* @__PURE__ */ jsx17("p", { className: "forma-page__description", children: description }),
814
+ children
815
+ ] });
816
+ }
817
+
818
+ // src/defaults/componentMap.ts
819
+ var defaultComponentMap = {
820
+ text: TextInput,
821
+ email: TextInput,
822
+ phone: TextInput,
823
+ url: TextInput,
824
+ password: TextInput,
825
+ textarea: TextareaInput,
826
+ number: NumberInput,
827
+ integer: IntegerInput,
828
+ boolean: BooleanInput,
829
+ date: DateInput,
830
+ datetime: DateTimeInput,
831
+ select: SelectInput,
832
+ multiselect: MultiSelectInput,
833
+ array: ArrayField,
834
+ object: ObjectField,
835
+ computed: ComputedDisplay,
836
+ display: DisplayField,
837
+ matrix: MatrixField,
838
+ fallback: FallbackField
839
+ };
840
+ var defaultFieldWrapper = FieldWrapper;
841
+ var defaultLayout = FormLayout;
842
+ var defaultWizardLayout = WizardLayout;
843
+ var defaultPageWrapper = PageWrapper;
844
+
845
+ // src/defaults/DefaultFormRenderer.tsx
846
+ import { jsx as jsx18 } from "react/jsx-runtime";
847
+ var DefaultFormRenderer = forwardRef(function DefaultFormRenderer2(props, ref) {
848
+ const {
849
+ components,
850
+ wizardLayout,
851
+ layout,
852
+ fieldWrapper,
853
+ pageWrapper,
854
+ ...rest
855
+ } = props;
856
+ return /* @__PURE__ */ jsx18(
857
+ FormRenderer,
858
+ {
859
+ ref,
860
+ components: components ?? defaultComponentMap,
861
+ fieldWrapper: fieldWrapper ?? defaultFieldWrapper,
862
+ layout: layout ?? (wizardLayout ? defaultWizardLayout : defaultLayout),
863
+ pageWrapper: pageWrapper ?? defaultPageWrapper,
864
+ ...rest
865
+ }
866
+ );
867
+ });
868
+ export {
869
+ ArrayField,
870
+ BooleanInput,
871
+ ComputedDisplay,
872
+ DateInput,
873
+ DateTimeInput,
874
+ DefaultFormRenderer,
875
+ DisplayField,
876
+ FallbackField,
877
+ FieldWrapper,
878
+ FormLayout,
879
+ IntegerInput,
880
+ MatrixField,
881
+ MultiSelectInput,
882
+ NumberInput,
883
+ ObjectField,
884
+ PageWrapper,
885
+ SelectInput,
886
+ TextInput,
887
+ TextareaInput,
888
+ WizardLayout,
889
+ defaultComponentMap,
890
+ defaultFieldWrapper,
891
+ defaultLayout,
892
+ defaultPageWrapper,
893
+ defaultWizardLayout
894
+ };
895
+ //# sourceMappingURL=index.js.map