@dynamic-field-kit/react 1.3.0 → 1.5.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.mjs CHANGED
@@ -61,17 +61,36 @@ function resolve(layout) {
61
61
  return { type: layout.type, config: layout };
62
62
  }
63
63
  function checkIsMobile(breakpoint) {
64
- return typeof window !== "undefined" && window.innerWidth < breakpoint;
64
+ if (typeof window === "undefined") {
65
+ return false;
66
+ }
67
+ if (typeof window.matchMedia === "function") {
68
+ return window.matchMedia(`(max-width: ${breakpoint - 1}px)`).matches;
69
+ }
70
+ return window.innerWidth < breakpoint;
65
71
  }
66
72
  layoutRegistry.register("responsive", ({ children, config }) => {
67
73
  const responsiveConfig = config;
68
74
  const breakpoint = responsiveConfig.breakpoint ?? 768;
69
75
  const [isMobile, setIsMobile] = useState(() => checkIsMobile(breakpoint));
70
76
  useEffect(() => {
71
- const handleResize = () => setIsMobile(checkIsMobile(breakpoint));
72
- handleResize();
73
- window.addEventListener("resize", handleResize);
74
- return () => window.removeEventListener("resize", handleResize);
77
+ if (typeof window === "undefined") {
78
+ return;
79
+ }
80
+ if (typeof window.matchMedia !== "function") {
81
+ const handleResize = () => setIsMobile(checkIsMobile(breakpoint));
82
+ window.addEventListener("resize", handleResize);
83
+ return () => window.removeEventListener("resize", handleResize);
84
+ }
85
+ const mediaQuery = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
86
+ const handleChange = (e) => setIsMobile(e.matches);
87
+ setIsMobile(mediaQuery.matches);
88
+ if (mediaQuery.addEventListener) {
89
+ mediaQuery.addEventListener("change", handleChange);
90
+ return () => mediaQuery.removeEventListener("change", handleChange);
91
+ }
92
+ mediaQuery.addListener(handleChange);
93
+ return () => mediaQuery.removeListener(handleChange);
75
94
  }, [breakpoint]);
76
95
  const current = isMobile ? responsiveConfig.mobile : responsiveConfig.desktop;
77
96
  if (!current) {
@@ -92,7 +111,311 @@ layoutRegistry.register("responsive", ({ children, config }) => {
92
111
  });
93
112
 
94
113
  // src/components/DynamicInput.tsx
95
- import React2, { useMemo } from "react";
114
+ import React3, { useMemo } from "react";
115
+
116
+ // src/defaultRenderers.tsx
117
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
118
+ var DefaultTextRenderer = ({
119
+ value,
120
+ onValueChange,
121
+ onBlur,
122
+ disabled,
123
+ readOnly,
124
+ required,
125
+ placeholder,
126
+ id,
127
+ className,
128
+ ariaInvalid,
129
+ ariaDescribedBy,
130
+ ariaRequired,
131
+ inputType = "text"
132
+ }) => /* @__PURE__ */ jsx3(
133
+ "input",
134
+ {
135
+ type: inputType,
136
+ id,
137
+ className,
138
+ value: value ?? "",
139
+ onChange: (e) => onValueChange?.(e.target.value),
140
+ onBlur,
141
+ disabled,
142
+ readOnly,
143
+ required,
144
+ placeholder,
145
+ "aria-invalid": ariaInvalid,
146
+ "aria-describedby": ariaDescribedBy,
147
+ "aria-required": ariaRequired
148
+ }
149
+ );
150
+ var DefaultNumberRenderer = ({
151
+ value,
152
+ onValueChange,
153
+ onBlur,
154
+ disabled,
155
+ readOnly,
156
+ required,
157
+ placeholder,
158
+ id,
159
+ className,
160
+ ariaInvalid,
161
+ ariaDescribedBy,
162
+ ariaRequired
163
+ }) => /* @__PURE__ */ jsx3(
164
+ "input",
165
+ {
166
+ type: "number",
167
+ id,
168
+ className,
169
+ value: value ?? "",
170
+ onChange: (e) => onValueChange?.(
171
+ e.target.value === "" ? void 0 : Number(e.target.value)
172
+ ),
173
+ onBlur,
174
+ disabled,
175
+ readOnly,
176
+ required,
177
+ placeholder,
178
+ "aria-invalid": ariaInvalid,
179
+ "aria-describedby": ariaDescribedBy,
180
+ "aria-required": ariaRequired
181
+ }
182
+ );
183
+ var DefaultPasswordRenderer = (props) => /* @__PURE__ */ jsx3(DefaultTextRenderer, { ...props, inputType: "password" });
184
+ var DefaultEmailRenderer = (props) => /* @__PURE__ */ jsx3(DefaultTextRenderer, { ...props, inputType: "email" });
185
+ var DefaultTextareaRenderer = ({
186
+ value,
187
+ onValueChange,
188
+ onBlur,
189
+ disabled,
190
+ readOnly,
191
+ required,
192
+ placeholder,
193
+ id,
194
+ className,
195
+ ariaInvalid,
196
+ ariaDescribedBy,
197
+ ariaRequired
198
+ }) => /* @__PURE__ */ jsx3(
199
+ "textarea",
200
+ {
201
+ id,
202
+ className,
203
+ value: value ?? "",
204
+ onChange: (e) => onValueChange?.(e.target.value),
205
+ onBlur,
206
+ disabled,
207
+ readOnly,
208
+ required,
209
+ placeholder,
210
+ "aria-invalid": ariaInvalid,
211
+ "aria-describedby": ariaDescribedBy,
212
+ "aria-required": ariaRequired
213
+ }
214
+ );
215
+ var DefaultCheckboxRenderer = ({
216
+ value,
217
+ onValueChange,
218
+ onBlur,
219
+ disabled,
220
+ readOnly,
221
+ required,
222
+ id,
223
+ className,
224
+ ariaInvalid,
225
+ ariaDescribedBy,
226
+ ariaRequired
227
+ }) => /* @__PURE__ */ jsx3(
228
+ "input",
229
+ {
230
+ type: "checkbox",
231
+ id,
232
+ className,
233
+ checked: Boolean(value),
234
+ onChange: (e) => onValueChange?.(e.target.checked),
235
+ onBlur,
236
+ disabled: disabled || readOnly,
237
+ required,
238
+ "aria-invalid": ariaInvalid,
239
+ "aria-describedby": ariaDescribedBy,
240
+ "aria-required": ariaRequired
241
+ }
242
+ );
243
+ var DefaultSelectRenderer = ({
244
+ value,
245
+ onValueChange,
246
+ onBlur,
247
+ disabled,
248
+ readOnly,
249
+ required,
250
+ options = [],
251
+ id,
252
+ className,
253
+ ariaInvalid,
254
+ ariaDescribedBy,
255
+ ariaRequired
256
+ }) => /* @__PURE__ */ jsxs2(
257
+ "select",
258
+ {
259
+ id,
260
+ className,
261
+ value: value ?? "",
262
+ onChange: (e) => onValueChange?.(e.target.value),
263
+ onBlur,
264
+ disabled: disabled || readOnly,
265
+ required,
266
+ "aria-invalid": ariaInvalid,
267
+ "aria-describedby": ariaDescribedBy,
268
+ "aria-required": ariaRequired,
269
+ children: [
270
+ /* @__PURE__ */ jsx3("option", { value: "", disabled: true, children: "-- Select --" }),
271
+ options.map((opt, i) => {
272
+ const optVal = opt.value ?? opt.id ?? opt;
273
+ const optLabel = opt.label ?? opt.name ?? String(optVal);
274
+ return /* @__PURE__ */ jsx3("option", { value: String(optVal), children: String(optLabel) }, String(optVal) + i);
275
+ })
276
+ ]
277
+ }
278
+ );
279
+ var DefaultRadioRenderer = ({
280
+ value,
281
+ onValueChange,
282
+ onBlur,
283
+ disabled,
284
+ readOnly,
285
+ required,
286
+ options = [],
287
+ id,
288
+ className,
289
+ ariaInvalid,
290
+ ariaDescribedBy
291
+ }) => /* @__PURE__ */ jsx3("div", { className: `dfk-radio-group ${className || ""}`, id, onBlur, children: options.map((opt, i) => {
292
+ const optVal = opt.value ?? opt.id ?? opt;
293
+ const optLabel = opt.label ?? opt.name ?? String(optVal);
294
+ const isChecked = String(value) === String(optVal);
295
+ const radioId = `${id || "radio"}-${i}`;
296
+ return /* @__PURE__ */ jsxs2(
297
+ "label",
298
+ {
299
+ htmlFor: radioId,
300
+ style: {
301
+ marginRight: "12px",
302
+ display: "inline-flex",
303
+ alignItems: "center"
304
+ },
305
+ children: [
306
+ /* @__PURE__ */ jsx3(
307
+ "input",
308
+ {
309
+ type: "radio",
310
+ id: radioId,
311
+ name: id,
312
+ value: String(optVal),
313
+ checked: isChecked,
314
+ onChange: () => onValueChange?.(optVal),
315
+ disabled: disabled || readOnly,
316
+ required,
317
+ "aria-invalid": ariaInvalid,
318
+ "aria-describedby": ariaDescribedBy
319
+ }
320
+ ),
321
+ /* @__PURE__ */ jsx3("span", { style: { marginLeft: "4px" }, children: String(optLabel) })
322
+ ]
323
+ },
324
+ String(optVal) + i
325
+ );
326
+ }) });
327
+ var DefaultRangeRenderer = ({
328
+ value,
329
+ onValueChange,
330
+ onBlur,
331
+ disabled,
332
+ readOnly,
333
+ required,
334
+ min,
335
+ max,
336
+ step,
337
+ id,
338
+ className,
339
+ ariaInvalid,
340
+ ariaDescribedBy
341
+ }) => /* @__PURE__ */ jsx3(
342
+ "input",
343
+ {
344
+ type: "range",
345
+ id,
346
+ className,
347
+ value: value ?? min ?? 0,
348
+ min,
349
+ max,
350
+ step,
351
+ onChange: (e) => onValueChange?.(Number(e.target.value)),
352
+ onBlur,
353
+ disabled: disabled || readOnly,
354
+ required,
355
+ "aria-invalid": ariaInvalid,
356
+ "aria-describedby": ariaDescribedBy
357
+ }
358
+ );
359
+ var DefaultFileRenderer = ({
360
+ onValueChange,
361
+ onBlur,
362
+ disabled,
363
+ readOnly,
364
+ required,
365
+ accept,
366
+ multiple,
367
+ id,
368
+ className,
369
+ ariaInvalid,
370
+ ariaDescribedBy
371
+ }) => /* @__PURE__ */ jsx3(
372
+ "input",
373
+ {
374
+ type: "file",
375
+ id,
376
+ className,
377
+ accept,
378
+ multiple,
379
+ onChange: (e) => {
380
+ const files = e.target.files;
381
+ if (!files) {
382
+ return;
383
+ }
384
+ onValueChange?.(multiple ? Array.from(files) : files[0] || null);
385
+ },
386
+ onBlur,
387
+ disabled: disabled || readOnly,
388
+ required,
389
+ "aria-invalid": ariaInvalid,
390
+ "aria-describedby": ariaDescribedBy
391
+ }
392
+ );
393
+ var DefaultDateRenderer = (props) => /* @__PURE__ */ jsx3(DefaultTextRenderer, { ...props, inputType: "date" });
394
+ var DefaultTimeRenderer = (props) => /* @__PURE__ */ jsx3(DefaultTextRenderer, { ...props, inputType: "time" });
395
+ var DefaultDateTimeLocalRenderer = (props) => /* @__PURE__ */ jsx3(DefaultTextRenderer, { ...props, inputType: "datetime-local" });
396
+ var DefaultSwitchRenderer = (props) => /* @__PURE__ */ jsx3(DefaultCheckboxRenderer, { ...props });
397
+ var defaultRenderersMap = {
398
+ text: DefaultTextRenderer,
399
+ number: DefaultNumberRenderer,
400
+ password: DefaultPasswordRenderer,
401
+ email: DefaultEmailRenderer,
402
+ textarea: DefaultTextareaRenderer,
403
+ checkbox: DefaultCheckboxRenderer,
404
+ select: DefaultSelectRenderer,
405
+ radio: DefaultRadioRenderer,
406
+ range: DefaultRangeRenderer,
407
+ file: DefaultFileRenderer,
408
+ date: DefaultDateRenderer,
409
+ time: DefaultTimeRenderer,
410
+ "datetime-local": DefaultDateTimeLocalRenderer,
411
+ switch: DefaultSwitchRenderer
412
+ };
413
+ function getDefaultRenderer(type) {
414
+ return defaultRenderersMap[type];
415
+ }
416
+
417
+ // src/FieldRegistryContext.tsx
418
+ import { createContext, useContext } from "react";
96
419
 
97
420
  // src/fieldRegistry.ts
98
421
  import {
@@ -100,43 +423,85 @@ import {
100
423
  } from "@dynamic-field-kit/core";
101
424
  var fieldRegistry = coreFieldRegistry;
102
425
 
426
+ // src/FieldRegistryContext.tsx
427
+ import { jsx as jsx4 } from "react/jsx-runtime";
428
+ var FieldRegistryContext = createContext(fieldRegistry);
429
+ var FieldRegistryProvider = ({
430
+ registry,
431
+ children
432
+ }) => /* @__PURE__ */ jsx4(FieldRegistryContext.Provider, { value: registry, children });
433
+ function useFieldRegistry() {
434
+ return useContext(FieldRegistryContext);
435
+ }
436
+
103
437
  // src/components/DynamicInput.tsx
104
- import { jsxs as jsxs2 } from "react/jsx-runtime";
438
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
105
439
  var DynamicInputInner = ({
106
440
  type,
107
441
  value,
108
442
  onChange,
443
+ onBlur,
109
444
  label,
110
445
  options,
111
446
  className,
112
- description
447
+ description,
448
+ disabled,
449
+ readOnly,
450
+ required,
451
+ touched,
452
+ dirty,
453
+ error,
454
+ id,
455
+ ariaInvalid,
456
+ ariaDescribedBy,
457
+ ariaRequired,
458
+ extraProps
113
459
  }) => {
460
+ const registry = useFieldRegistry();
114
461
  const Renderer = useMemo(
115
- () => fieldRegistry.get(type),
116
- [type]
462
+ () => registry.get(type) || getDefaultRenderer(type),
463
+ [registry, type]
117
464
  );
118
465
  if (!Renderer) {
119
- return /* @__PURE__ */ jsxs2("div", { children: [
466
+ return /* @__PURE__ */ jsxs3("div", { children: [
120
467
  "Unknown field type: ",
121
468
  type
122
469
  ] });
123
470
  }
124
- return React2.createElement(Renderer, {
471
+ return React3.createElement(Renderer, {
472
+ ...extraProps,
125
473
  value,
126
474
  onValueChange: onChange,
475
+ onBlur,
127
476
  label,
128
477
  options,
129
478
  className,
130
- description
479
+ description,
480
+ disabled,
481
+ readOnly,
482
+ required,
483
+ touched,
484
+ dirty,
485
+ error,
486
+ id,
487
+ ariaInvalid,
488
+ ariaDescribedBy,
489
+ ariaRequired
131
490
  });
132
491
  };
133
- var DynamicInput = React2.memo(
492
+ var DynamicInput = /* @__PURE__ */ React3.memo(
134
493
  DynamicInputInner
135
494
  );
136
495
  var DynamicInput_default = DynamicInput;
137
496
 
138
497
  // src/components/FieldInput.tsx
139
- import React5, { useCallback as useCallback3 } from "react";
498
+ import {
499
+ resolveDisabled,
500
+ resolveOptions,
501
+ resolveReadOnly,
502
+ validateField
503
+ } from "@dynamic-field-kit/core";
504
+ import React6, { useCallback as useCallback3 } from "react";
140
505
 
141
506
  // src/components/FieldGroupInput.tsx
142
507
  import {
@@ -148,7 +513,8 @@ import { useCallback as useCallback2 } from "react";
148
513
 
149
514
  // src/components/MultiFieldInput.tsx
150
515
  import {
151
- applyComputedValues
516
+ applyComputedValues,
517
+ validateFields
152
518
  } from "@dynamic-field-kit/core";
153
519
  import {
154
520
  useCallback,
@@ -157,7 +523,7 @@ import {
157
523
  useRef,
158
524
  useState as useState2
159
525
  } from "react";
160
- import { jsx as jsx3 } from "react/jsx-runtime";
526
+ import { jsx as jsx5 } from "react/jsx-runtime";
161
527
  function resolveLayout(layout) {
162
528
  if (!layout) {
163
529
  return { type: "column", config: {} };
@@ -171,19 +537,27 @@ var MultiFieldInput = ({
171
537
  fieldDescriptions,
172
538
  properties,
173
539
  onChange,
174
- layout
540
+ layout,
541
+ rootData,
542
+ onValidityChange,
543
+ onBlurField
175
544
  }) => {
176
545
  const [data, setData] = useState2({});
546
+ const [touchedFields, setTouchedFields] = useState2(
547
+ {}
548
+ );
549
+ const initialPropertiesRef = useRef(properties ?? {});
177
550
  useEffect2(() => {
178
551
  if (properties) {
179
- setData(applyComputedValues(fieldDescriptions, properties));
552
+ setData(applyComputedValues(fieldDescriptions, properties, rootData));
180
553
  }
181
554
  }, [properties]);
555
+ const effectiveRoot = rootData ?? data;
182
556
  const visibleFields = useMemo2(
183
557
  () => fieldDescriptions.filter(
184
- (f) => !f.appearCondition || f.appearCondition(data)
558
+ (f) => !f.appearCondition || f.appearCondition(data, effectiveRoot)
185
559
  ),
186
- [fieldDescriptions, data]
560
+ [fieldDescriptions, data, effectiveRoot]
187
561
  );
188
562
  const dataRef = useRef(data);
189
563
  dataRef.current = data;
@@ -191,25 +565,46 @@ var MultiFieldInput = ({
191
565
  onChangeRef.current = onChange;
192
566
  const fieldDescriptionsRef = useRef(fieldDescriptions);
193
567
  fieldDescriptionsRef.current = fieldDescriptions;
568
+ const rootDataRef = useRef(rootData);
569
+ rootDataRef.current = rootData;
570
+ const onValidityChangeRef = useRef(onValidityChange);
571
+ onValidityChangeRef.current = onValidityChange;
572
+ const onBlurFieldRef = useRef(onBlurField);
573
+ onBlurFieldRef.current = onBlurField;
574
+ useEffect2(() => {
575
+ onValidityChangeRef.current?.(
576
+ validateFields(fieldDescriptions, data, rootData)
577
+ );
578
+ }, [data, fieldDescriptions, rootData]);
194
579
  const handleValueChangeField = useCallback((value, key) => {
195
- const next = applyComputedValues(fieldDescriptionsRef.current, {
196
- ...dataRef.current,
197
- [key]: value
198
- });
580
+ const merged = { ...dataRef.current, [key]: value };
581
+ const next = applyComputedValues(
582
+ fieldDescriptionsRef.current,
583
+ merged,
584
+ rootDataRef.current
585
+ );
199
586
  dataRef.current = next;
200
587
  setData(next);
201
588
  onChangeRef.current?.(next);
202
589
  }, []);
590
+ const handleBlurField = useCallback((key) => {
591
+ setTouchedFields((prev) => prev[key] ? prev : { ...prev, [key]: true });
592
+ onBlurFieldRef.current?.(key);
593
+ }, []);
203
594
  const { type, config } = resolveLayout(layout);
204
595
  const Layout = layoutRegistry.get(type);
205
596
  if (!Layout) {
206
597
  throw new Error(`Unknown layout: ${type}`);
207
598
  }
208
- return /* @__PURE__ */ jsx3(Layout, { config, children: visibleFields.map((f) => /* @__PURE__ */ jsx3(
599
+ return /* @__PURE__ */ jsx5(Layout, { config, children: visibleFields.map((f) => /* @__PURE__ */ jsx5(
209
600
  FieldInput_default,
210
601
  {
211
602
  fieldDescription: f,
212
603
  renderInfos: data,
604
+ rootData: effectiveRoot,
605
+ touched: Boolean(touchedFields[f.name]),
606
+ dirty: data[f.name] !== initialPropertiesRef.current[f.name],
607
+ onBlurField: handleBlurField,
213
608
  onValueChangeField: handleValueChangeField
214
609
  },
215
610
  f.name
@@ -218,9 +613,24 @@ var MultiFieldInput = ({
218
613
  var MultiFieldInput_default = MultiFieldInput;
219
614
 
220
615
  // src/components/FieldGroupInput.tsx
221
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
222
- var FieldGroupInput = ({ fieldDescription, items, onChange }) => {
223
- const { fields = [], label, addLabel, removeLabel } = fieldDescription;
616
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
617
+ var FieldGroupInput = ({
618
+ fieldDescription,
619
+ items,
620
+ rootData,
621
+ onChange
622
+ }) => {
623
+ const {
624
+ fields = [],
625
+ label,
626
+ addLabel,
627
+ removeLabel,
628
+ keyField
629
+ } = fieldDescription;
630
+ const itemKey = (item, index) => keyField ? item[keyField] ?? index : index;
631
+ const addText = addLabel ?? "Add";
632
+ const removeText = removeLabel ?? "Remove";
633
+ const groupName = label ?? fieldDescription.name;
224
634
  const handleItemChange = useCallback2(
225
635
  (index, next) => {
226
636
  const nextItems = items.slice();
@@ -244,41 +654,44 @@ var FieldGroupInput = ({ fieldDescription, items, onChange }) => {
244
654
  },
245
655
  [fieldDescription, items, onChange]
246
656
  );
247
- return /* @__PURE__ */ jsxs3("div", { className: fieldDescription.className, children: [
248
- label && /* @__PURE__ */ jsx4("div", { children: label }),
249
- items.map((item, index) => /* @__PURE__ */ jsxs3(
657
+ return /* @__PURE__ */ jsxs4("div", { className: fieldDescription.className, children: [
658
+ label && /* @__PURE__ */ jsx6("div", { children: label }),
659
+ items.map((item, index) => /* @__PURE__ */ jsxs4(
250
660
  "div",
251
661
  {
252
662
  style: { display: "flex", alignItems: "flex-start", gap: 8 },
253
663
  children: [
254
- /* @__PURE__ */ jsx4("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsx4(
664
+ /* @__PURE__ */ jsx6("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsx6(
255
665
  MultiFieldInput_default,
256
666
  {
257
667
  fieldDescriptions: fields,
258
668
  properties: item,
669
+ rootData,
259
670
  onChange: (next) => handleItemChange(index, next)
260
671
  }
261
672
  ) }),
262
- /* @__PURE__ */ jsx4(
673
+ /* @__PURE__ */ jsx6(
263
674
  "button",
264
675
  {
265
676
  type: "button",
677
+ "aria-label": `${removeText} ${groupName} ${index + 1}`,
266
678
  onClick: () => handleRemove(index),
267
679
  disabled: !canRemoveGroupItem(fieldDescription, items),
268
- children: removeLabel ?? "Remove"
680
+ children: removeText
269
681
  }
270
682
  )
271
683
  ]
272
684
  },
273
- index
685
+ itemKey(item, index)
274
686
  )),
275
- /* @__PURE__ */ jsx4(
687
+ /* @__PURE__ */ jsx6(
276
688
  "button",
277
689
  {
278
690
  type: "button",
691
+ "aria-label": `${addText} ${groupName}`,
279
692
  onClick: handleAdd,
280
693
  disabled: !canAddGroupItem(fieldDescription, items),
281
- children: addLabel ?? "Add"
694
+ children: addText
282
695
  }
283
696
  )
284
697
  ] });
@@ -286,51 +699,422 @@ var FieldGroupInput = ({ fieldDescription, items, onChange }) => {
286
699
  var FieldGroupInput_default = FieldGroupInput;
287
700
 
288
701
  // src/components/FieldInput.tsx
289
- import { jsx as jsx5 } from "react/jsx-runtime";
702
+ import { jsx as jsx7 } from "react/jsx-runtime";
290
703
  var FieldInputInner = ({
291
704
  fieldDescription,
292
705
  renderInfos,
706
+ rootData,
707
+ touched,
708
+ dirty,
709
+ onBlurField,
293
710
  onValueChangeField
294
711
  }) => {
295
- const { name, type, label, options, className, description, fields } = fieldDescription;
712
+ const { name, type, label, className, description, props, fields, required } = fieldDescription;
296
713
  const handleChange = useCallback3(
297
714
  (v) => onValueChangeField(v, name),
298
715
  [onValueChangeField, name]
299
716
  );
717
+ const handleBlur = useCallback3(
718
+ () => onBlurField?.(name),
719
+ [onBlurField, name]
720
+ );
300
721
  if (fields) {
301
722
  const items = Array.isArray(renderInfos[name]) ? renderInfos[name] : [];
302
- return /* @__PURE__ */ jsx5(
723
+ return /* @__PURE__ */ jsx7(
303
724
  FieldGroupInput_default,
304
725
  {
305
726
  fieldDescription,
306
727
  items,
728
+ rootData,
307
729
  onChange: handleChange
308
730
  }
309
731
  );
310
732
  }
311
- return /* @__PURE__ */ jsx5(
733
+ const effectiveDisabled = resolveDisabled(
734
+ fieldDescription,
735
+ renderInfos,
736
+ rootData
737
+ );
738
+ const readOnly = resolveReadOnly(fieldDescription, renderInfos, rootData);
739
+ const resolvedOptionsList = resolveOptions(
740
+ fieldDescription,
741
+ renderInfos,
742
+ rootData
743
+ );
744
+ const errors = effectiveDisabled ? [] : validateField(fieldDescription, renderInfos[name], renderInfos, rootData);
745
+ const error = errors.length > 0 ? errors : void 0;
746
+ const fieldId = `dfk-field-${name}`;
747
+ return /* @__PURE__ */ jsx7(
312
748
  DynamicInput_default,
313
749
  {
750
+ id: fieldId,
314
751
  type,
315
752
  label,
316
753
  value: renderInfos[name],
317
- options,
754
+ options: resolvedOptionsList,
318
755
  className,
319
756
  description,
320
- onChange: handleChange
757
+ disabled: effectiveDisabled,
758
+ readOnly,
759
+ required,
760
+ touched,
761
+ dirty,
762
+ error,
763
+ ariaInvalid: Boolean(error),
764
+ ariaRequired: Boolean(required),
765
+ extraProps: props,
766
+ onChange: handleChange,
767
+ onBlur: handleBlur
321
768
  }
322
769
  );
323
770
  };
324
- var FieldInput = React5.memo(FieldInputInner, (prev, next) => {
771
+ var FieldInput = /* @__PURE__ */ React6.memo(FieldInputInner, (prev, next) => {
325
772
  const name = prev.fieldDescription.name;
326
- return prev.fieldDescription === next.fieldDescription && prev.onValueChangeField === next.onValueChangeField && prev.renderInfos[name] === next.renderInfos[name];
773
+ return prev.fieldDescription === next.fieldDescription && prev.onValueChangeField === next.onValueChangeField && prev.onBlurField === next.onBlurField && prev.rootData === next.rootData && prev.touched === next.touched && prev.dirty === next.dirty && prev.renderInfos[name] === next.renderInfos[name];
327
774
  });
328
775
  var FieldInput_default = FieldInput;
776
+
777
+ // src/components/DynamicFormDevTools.tsx
778
+ import { useState as useState3 } from "react";
779
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
780
+ var DynamicFormDevTools = ({
781
+ data,
782
+ errors = {},
783
+ touched = {},
784
+ isDirty = false,
785
+ fields = [],
786
+ position = "bottom-right"
787
+ }) => {
788
+ const [isOpen, setIsOpen] = useState3(false);
789
+ const [activeTab, setActiveTab] = useState3("data");
790
+ const errorCount = Object.keys(errors).length;
791
+ const posStyle = position === "bottom-left" ? { left: "16px", bottom: "16px" } : { right: "16px", bottom: "16px" };
792
+ if (!isOpen) {
793
+ return /* @__PURE__ */ jsxs5(
794
+ "button",
795
+ {
796
+ type: "button",
797
+ onClick: () => setIsOpen(true),
798
+ style: {
799
+ position: "fixed",
800
+ ...posStyle,
801
+ zIndex: 99999,
802
+ background: "#1e293b",
803
+ color: "#f8fafc",
804
+ border: "1px solid #334155",
805
+ borderRadius: "20px",
806
+ padding: "8px 14px",
807
+ fontSize: "12px",
808
+ fontWeight: 600,
809
+ cursor: "pointer",
810
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
811
+ display: "flex",
812
+ alignItems: "center",
813
+ gap: "6px"
814
+ },
815
+ children: [
816
+ /* @__PURE__ */ jsx8("span", { children: "\u{1F50D} DevTools" }),
817
+ errorCount > 0 && /* @__PURE__ */ jsx8(
818
+ "span",
819
+ {
820
+ style: {
821
+ background: "#ef4444",
822
+ color: "#fff",
823
+ borderRadius: "10px",
824
+ padding: "2px 6px",
825
+ fontSize: "10px"
826
+ },
827
+ children: errorCount
828
+ }
829
+ )
830
+ ]
831
+ }
832
+ );
833
+ }
834
+ return /* @__PURE__ */ jsxs5(
835
+ "div",
836
+ {
837
+ style: {
838
+ position: "fixed",
839
+ ...posStyle,
840
+ zIndex: 99999,
841
+ width: "360px",
842
+ maxHeight: "420px",
843
+ background: "#0f172a",
844
+ color: "#f8fafc",
845
+ border: "1px solid #334155",
846
+ borderRadius: "12px",
847
+ boxShadow: "0 10px 25px rgba(0,0,0,0.3)",
848
+ display: "flex",
849
+ flexDirection: "column",
850
+ fontFamily: "monospace, sans-serif",
851
+ fontSize: "12px",
852
+ overflow: "hidden"
853
+ },
854
+ children: [
855
+ /* @__PURE__ */ jsxs5(
856
+ "div",
857
+ {
858
+ style: {
859
+ padding: "10px 14px",
860
+ background: "#1e293b",
861
+ display: "flex",
862
+ justifyContent: "space-between",
863
+ alignItems: "center",
864
+ borderBottom: "1px solid #334155"
865
+ },
866
+ children: [
867
+ /* @__PURE__ */ jsx8("span", { style: { fontWeight: "bold", color: "#38bdf8" }, children: "\u{1F6E0}\uFE0F Form DevTools" }),
868
+ /* @__PURE__ */ jsx8(
869
+ "button",
870
+ {
871
+ type: "button",
872
+ onClick: () => setIsOpen(false),
873
+ style: {
874
+ background: "transparent",
875
+ border: "none",
876
+ color: "#94a3b8",
877
+ fontSize: "14px",
878
+ cursor: "pointer"
879
+ },
880
+ children: "\u2715"
881
+ }
882
+ )
883
+ ]
884
+ }
885
+ ),
886
+ /* @__PURE__ */ jsx8(
887
+ "div",
888
+ {
889
+ style: {
890
+ display: "flex",
891
+ background: "#1e293b",
892
+ borderBottom: "1px solid #334155"
893
+ },
894
+ children: ["data", "errors", "meta", "fields"].map((tab) => /* @__PURE__ */ jsxs5(
895
+ "button",
896
+ {
897
+ type: "button",
898
+ onClick: () => setActiveTab(tab),
899
+ style: {
900
+ flex: 1,
901
+ padding: "6px 0",
902
+ background: activeTab === tab ? "#0f172a" : "transparent",
903
+ color: activeTab === tab ? "#38bdf8" : "#94a3b8",
904
+ border: "none",
905
+ cursor: "pointer",
906
+ textTransform: "capitalize",
907
+ fontSize: "11px",
908
+ fontWeight: activeTab === tab ? "bold" : "normal"
909
+ },
910
+ children: [
911
+ tab,
912
+ " ",
913
+ tab === "errors" && errorCount > 0 ? `(${errorCount})` : ""
914
+ ]
915
+ },
916
+ tab
917
+ ))
918
+ }
919
+ ),
920
+ /* @__PURE__ */ jsxs5("div", { style: { padding: "12px", overflowY: "auto", flex: 1 }, children: [
921
+ activeTab === "data" && /* @__PURE__ */ jsx8(
922
+ "pre",
923
+ {
924
+ style: {
925
+ margin: 0,
926
+ whiteSpace: "pre-wrap",
927
+ wordBreak: "break-all",
928
+ color: "#a7f3d0"
929
+ },
930
+ children: JSON.stringify(data, null, 2)
931
+ }
932
+ ),
933
+ activeTab === "errors" && /* @__PURE__ */ jsx8("div", { children: Object.keys(errors).length === 0 ? /* @__PURE__ */ jsx8("span", { style: { color: "#4ade80" }, children: "\u2713 No validation errors" }) : Object.entries(errors).map(([field, msgs]) => /* @__PURE__ */ jsxs5("div", { style: { marginBottom: "8px" }, children: [
934
+ /* @__PURE__ */ jsxs5("span", { style: { color: "#f87171", fontWeight: "bold" }, children: [
935
+ field,
936
+ ":"
937
+ ] }),
938
+ /* @__PURE__ */ jsx8("ul", { style: { margin: "4px 0 0 16px", padding: 0 }, children: msgs.map((m, i) => /* @__PURE__ */ jsx8("li", { style: { color: "#fca5a5" }, children: m }, i)) })
939
+ ] }, field)) }),
940
+ activeTab === "meta" && /* @__PURE__ */ jsxs5("div", { children: [
941
+ /* @__PURE__ */ jsxs5("div", { style: { marginBottom: "6px" }, children: [
942
+ /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "isDirty: " }),
943
+ /* @__PURE__ */ jsx8("span", { style: { color: isDirty ? "#facc15" : "#4ade80" }, children: String(isDirty) })
944
+ ] }),
945
+ /* @__PURE__ */ jsxs5("div", { children: [
946
+ /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "Touched Fields:" }),
947
+ /* @__PURE__ */ jsx8("pre", { style: { margin: "4px 0 0 0", color: "#cbd5e1" }, children: JSON.stringify(touched, null, 2) })
948
+ ] })
949
+ ] }),
950
+ activeTab === "fields" && /* @__PURE__ */ jsx8("div", { children: fields.length === 0 ? /* @__PURE__ */ jsx8("span", { style: { color: "#94a3b8" }, children: "No field descriptions passed" }) : fields.map((f) => /* @__PURE__ */ jsxs5(
951
+ "div",
952
+ {
953
+ style: {
954
+ padding: "6px",
955
+ marginBottom: "6px",
956
+ background: "#1e293b",
957
+ borderRadius: "4px"
958
+ },
959
+ children: [
960
+ /* @__PURE__ */ jsx8("div", { style: { color: "#38bdf8", fontWeight: "bold" }, children: f.name }),
961
+ /* @__PURE__ */ jsxs5("div", { style: { color: "#94a3b8", fontSize: "10px" }, children: [
962
+ "type: ",
963
+ f.type,
964
+ " | required: ",
965
+ String(Boolean(f.required))
966
+ ] })
967
+ ]
968
+ },
969
+ f.name
970
+ )) })
971
+ ] })
972
+ ]
973
+ }
974
+ );
975
+ };
976
+
977
+ // src/useDynamicForm.ts
978
+ import {
979
+ applyComputedValues as applyComputedValues2,
980
+ validateFields as validateFields2
981
+ } from "@dynamic-field-kit/core";
982
+ import { useCallback as useCallback4, useState as useState4 } from "react";
983
+ function useDynamicForm({
984
+ fields,
985
+ initialValues = {},
986
+ validateOnBlur = true,
987
+ validateOnChange = false
988
+ }) {
989
+ const [data, setData] = useState4(
990
+ () => applyComputedValues2(fields, initialValues)
991
+ );
992
+ const [errors, setErrors] = useState4({});
993
+ const [isDirty, setIsDirty] = useState4(false);
994
+ const [touched, setTouched] = useState4({});
995
+ const [isSubmitting, setIsSubmitting] = useState4(false);
996
+ const [isSubmitted, setIsSubmitted] = useState4(false);
997
+ const validate = useCallback4(() => {
998
+ const res = validateFields2(fields, data);
999
+ setErrors(res.errors);
1000
+ return res.valid;
1001
+ }, [fields, data]);
1002
+ const handleChange = useCallback4(
1003
+ (newData) => {
1004
+ const next = applyComputedValues2(fields, newData);
1005
+ setData(next);
1006
+ setIsDirty(true);
1007
+ if (validateOnChange) {
1008
+ const res = validateFields2(fields, next);
1009
+ setErrors(res.errors);
1010
+ }
1011
+ },
1012
+ [fields, validateOnChange]
1013
+ );
1014
+ const setFieldValue = useCallback4(
1015
+ (name, value) => {
1016
+ handleChange({ ...data, [name]: value });
1017
+ },
1018
+ [data, handleChange]
1019
+ );
1020
+ const setFieldTouched = useCallback4((name, isTouched = true) => {
1021
+ setTouched((prev) => ({ ...prev, [name]: isTouched }));
1022
+ }, []);
1023
+ const handleBlur = useCallback4(
1024
+ (fieldName) => {
1025
+ setFieldTouched(fieldName, true);
1026
+ if (validateOnBlur) {
1027
+ const res = validateFields2(fields, data);
1028
+ setErrors(res.errors);
1029
+ }
1030
+ },
1031
+ [fields, data, validateOnBlur, setFieldTouched]
1032
+ );
1033
+ const reset = useCallback4(
1034
+ (newValues) => {
1035
+ const seed = newValues ?? initialValues;
1036
+ const next = applyComputedValues2(fields, seed);
1037
+ setData(next);
1038
+ setErrors({});
1039
+ setIsDirty(false);
1040
+ setTouched({});
1041
+ setIsSubmitting(false);
1042
+ setIsSubmitted(false);
1043
+ },
1044
+ [fields, initialValues]
1045
+ );
1046
+ const handleSubmit = useCallback4(
1047
+ (onValid, onInvalid) => async (e) => {
1048
+ if (e && typeof e.preventDefault === "function") {
1049
+ e.preventDefault();
1050
+ }
1051
+ setIsSubmitting(true);
1052
+ try {
1053
+ const res = validateFields2(fields, data);
1054
+ setErrors(res.errors);
1055
+ setIsSubmitted(true);
1056
+ if (res.valid) {
1057
+ await onValid(data);
1058
+ } else if (onInvalid) {
1059
+ onInvalid(res.errors);
1060
+ }
1061
+ } finally {
1062
+ setIsSubmitting(false);
1063
+ }
1064
+ },
1065
+ [fields, data]
1066
+ );
1067
+ const isValid = Object.keys(errors).length === 0;
1068
+ return {
1069
+ data,
1070
+ errors,
1071
+ isValid,
1072
+ isDirty,
1073
+ isSubmitting,
1074
+ isSubmitted,
1075
+ touched,
1076
+ setData,
1077
+ setFieldValue,
1078
+ setFieldTouched,
1079
+ handleChange,
1080
+ handleBlur,
1081
+ reset,
1082
+ validate,
1083
+ handleSubmit
1084
+ };
1085
+ }
1086
+
1087
+ // src/index.ts
1088
+ import { FieldRegistry } from "@dynamic-field-kit/core";
1089
+ import {
1090
+ validateField as validateField2,
1091
+ validateFieldAsync,
1092
+ validateFields as validateFields3,
1093
+ validateFieldsAsync,
1094
+ resolveDisabled as resolveDisabled2,
1095
+ resolveReadOnly as resolveReadOnly2,
1096
+ resolveOptions as resolveOptions2,
1097
+ validators
1098
+ } from "@dynamic-field-kit/core";
329
1099
  export {
1100
+ DynamicFormDevTools,
330
1101
  DynamicInput_default as DynamicInput,
331
1102
  FieldInput_default as FieldInput,
1103
+ FieldRegistry,
1104
+ FieldRegistryProvider,
332
1105
  MultiFieldInput_default as MultiFieldInput,
1106
+ defaultRenderersMap,
333
1107
  fieldRegistry,
334
- layoutRegistry
1108
+ getDefaultRenderer,
1109
+ layoutRegistry,
1110
+ resolveDisabled2 as resolveDisabled,
1111
+ resolveOptions2 as resolveOptions,
1112
+ resolveReadOnly2 as resolveReadOnly,
1113
+ useDynamicForm,
1114
+ useFieldRegistry,
1115
+ validateField2 as validateField,
1116
+ validateFieldAsync,
1117
+ validateFields3 as validateFields,
1118
+ validateFieldsAsync,
1119
+ validators
335
1120
  };
336
- //# sourceMappingURL=index.mjs.map