@k3-universe/react-kit 0.0.13 → 0.0.15

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 (59) hide show
  1. package/dist/index.js +1773 -1739
  2. package/dist/kit/builder/data-table/types.d.ts +1 -1
  3. package/dist/kit/builder/data-table/types.d.ts.map +1 -1
  4. package/dist/kit/builder/form/components/FormBuilder.d.ts +3 -172
  5. package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
  6. package/dist/kit/builder/form/components/FormBuilderContext.d.ts +18 -0
  7. package/dist/kit/builder/form/components/FormBuilderContext.d.ts.map +1 -0
  8. package/dist/kit/builder/form/components/FormBuilderField.d.ts +8 -8
  9. package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
  10. package/dist/kit/builder/form/components/fields/types.d.ts +3 -3
  11. package/dist/kit/builder/form/components/fields/types.d.ts.map +1 -1
  12. package/dist/kit/builder/form/components/sectionNodes.d.ts +17 -0
  13. package/dist/kit/builder/form/components/sectionNodes.d.ts.map +1 -0
  14. package/dist/kit/builder/form/index.d.ts +1 -0
  15. package/dist/kit/builder/form/index.d.ts.map +1 -1
  16. package/dist/kit/builder/form/types.d.ts +176 -0
  17. package/dist/kit/builder/form/types.d.ts.map +1 -0
  18. package/dist/kit/builder/form/utils/common-forms.d.ts +1 -1
  19. package/dist/kit/builder/form/utils/common-forms.d.ts.map +1 -1
  20. package/dist/kit/builder/form/utils/field-factories.d.ts +3 -3
  21. package/dist/kit/builder/form/utils/field-factories.d.ts.map +1 -1
  22. package/dist/kit/builder/form/utils/section-factories.d.ts +4 -4
  23. package/dist/kit/builder/form/utils/section-factories.d.ts.map +1 -1
  24. package/dist/kit/builder/stack-dialog/provider.d.ts.map +1 -1
  25. package/dist/kit/builder/stack-dialog/renderer.d.ts.map +1 -1
  26. package/dist/kit/components/autocomplete/Autocomplete.d.ts +8 -8
  27. package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
  28. package/dist/kit/components/autocomplete/types.d.ts +6 -4
  29. package/dist/kit/components/autocomplete/types.d.ts.map +1 -1
  30. package/dist/kit/themes/clean-slate.css +3 -3
  31. package/dist/kit/themes/default.css +4 -4
  32. package/dist/kit/themes/minimal-modern.css +3 -3
  33. package/dist/kit/themes/spotify.css +3 -3
  34. package/package.json +1 -1
  35. package/src/kit/builder/data-table/components/DataTable.tsx +1 -1
  36. package/src/kit/builder/data-table/types.ts +1 -1
  37. package/src/kit/builder/form/components/FormBuilder.tsx +113 -369
  38. package/src/kit/builder/form/components/FormBuilderContext.tsx +45 -0
  39. package/src/kit/builder/form/components/FormBuilderField.tsx +42 -34
  40. package/src/kit/builder/form/components/fields/AutocompleteField.tsx +2 -2
  41. package/src/kit/builder/form/components/fields/types.ts +3 -3
  42. package/src/kit/builder/form/components/sectionNodes.tsx +116 -0
  43. package/src/kit/builder/form/index.ts +1 -0
  44. package/src/kit/builder/form/types.ts +200 -0
  45. package/src/kit/builder/form/utils/common-forms.ts +1 -1
  46. package/src/kit/builder/form/utils/field-factories.ts +5 -5
  47. package/src/kit/builder/form/utils/section-factories.ts +10 -10
  48. package/src/kit/builder/stack-dialog/provider.tsx +2 -1
  49. package/src/kit/builder/stack-dialog/renderer.tsx +6 -7
  50. package/src/kit/components/autocomplete/Autocomplete.tsx +34 -26
  51. package/src/kit/components/autocomplete/types.ts +7 -5
  52. package/src/kit/themes/default.css +1 -1
  53. package/src/shadcn/ui/button.tsx +1 -1
  54. package/src/shadcn/ui/command.tsx +1 -1
  55. package/src/shadcn/ui/input.tsx +1 -1
  56. package/src/shadcn/ui/popover.tsx +1 -1
  57. package/src/shadcn/ui/select.tsx +1 -1
  58. package/src/shadcn/ui/textarea.tsx +1 -1
  59. package/src/stories/kit/builder/Form.MultipleFormBuilder.stories.tsx +335 -0
@@ -1,221 +1,22 @@
1
- import type React from 'react';
2
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
3
- import { useForm, useWatch, type Control, type FieldValues } from 'react-hook-form';
2
+ import { useForm, useWatch, type FieldValues, type Path } from 'react-hook-form';
4
3
  import { zodResolver } from '@hookform/resolvers/zod';
5
4
  import { z } from 'zod';
6
5
  import { cn } from '../../../../shadcn/lib/utils';
7
6
  import { Button } from '../../../../shadcn/ui/button';
8
- import { FormBuilderField } from './FormBuilderField';
9
7
  import SectionBuilder from '../../section/SectionBuilder';
8
+ import { buildSectionNodes } from './sectionNodes';
9
+ import { FormBuilderContext, type FormBuilderContextValue } from './FormBuilderContext';
10
10
  import type {
11
- SectionLayout,
12
- SectionGridOptions,
13
- SectionFlexOptions,
14
- SectionNode,
15
- } from '../../section/types';
16
- import type {
17
- AutocompleteFetcher,
18
- AutocompleteOption,
19
- } from '../../../components/autocomplete/types';
20
- import type { Accept } from 'react-dropzone';
21
- import type {
22
- FileRecord,
23
- FileUploaderLayout,
24
- } from '../../../components/fileuploader/types';
25
-
26
- export interface FormBuilderFieldConfig {
27
- id?: string; // Optional ID for test fixtures
28
- name: string;
29
- label: string;
30
- type:
31
- | 'text'
32
- | 'email'
33
- | 'password'
34
- | 'number'
35
- | 'textarea'
36
- | 'select'
37
- | 'autocomplete'
38
- | 'checkbox'
39
- | 'switch'
40
- | 'radio'
41
- | 'date' // native input date
42
- | 'date_picker' // UI DatePicker
43
- | 'date_range' // UI DateRangePicker
44
- | 'month' // UI MonthPicker (single month Date)
45
- | 'month_range' // UI MonthRangePicker { start: Date, end: Date }
46
- | 'time' // UI TimePicker (Date with time part)
47
- | 'time_range' // UI TimeRangePicker { from: Date, to: Date }
48
- | 'date_time' // UI DateTimePicker (Date with date+time)
49
- | 'date_time_range' // UI DateTimeRangePicker { from: Date, to: Date }
50
- | 'file'
51
- | 'object'
52
- | 'array';
53
- placeholder?: string;
54
- description?: string;
55
- required?: boolean;
56
- disabled?: boolean;
57
- options?: { label: string; value: string | number | null }[];
58
- // Autocomplete specific (client/server)
59
- autocompleteMode?: 'client' | 'server';
60
- fetcher?: AutocompleteFetcher;
61
- pageSize?: number;
62
- searchPlaceholder?: string;
63
- renderOption?: (
64
- option: AutocompleteOption,
65
- selected: boolean
66
- ) => React.ReactNode;
67
- // New autocomplete features
68
- multiple?: boolean;
69
- allowCustomValue?: boolean;
70
- chipVariant?: 'default' | 'secondary' | 'destructive' | 'outline';
71
- chipClassName?: string;
72
- clearable?: boolean;
73
- initialSelectedOptions?: AutocompleteOption | AutocompleteOption[] | null;
74
- loadSelected?: (values: Array<string | number>) => Promise<AutocompleteOption[]>;
75
- validation?:
76
- | z.ZodType<unknown>
77
- | {
78
- pattern?: { value: RegExp; message: string };
79
- min?: { value: number; message: string };
80
- max?: { value: number; message: string };
81
- minLength?: { value: number; message: string };
82
- maxLength?: { value: number; message: string };
83
- // For array-like fields (e.g., file uploader)
84
- minItems?: { value: number; message: string };
85
- maxItems?: { value: number; message: string };
86
- };
87
- defaultValue?: unknown;
88
- fields?: FormBuilderFieldConfig[]; // For nested object/array fields
89
- dependencies?: {
90
- field: string;
91
- condition: (value: unknown) => boolean;
92
- action: 'show' | 'hide' | 'enable' | 'disable' | 'setValue';
93
- value?: unknown;
94
- }[];
95
- onChange?: (
96
- value: unknown,
97
- setValue: (field: string, value: unknown) => void,
98
- getValues: () => Record<string, unknown>
99
- ) => void;
100
- className?: string;
101
- gridCols?: number;
102
- rows?: number; // For textarea fields
103
- itemType?: string; // For array fields
104
- // Array field layout: default 'card'
105
- arrayLayout?: 'card' | 'table' | 'custom';
106
- // Custom renderer for array fields when arrayLayout === 'custom'
107
- arrayRender?: (params: {
108
- field: FormBuilderFieldConfig;
109
- control: Control<FieldValues>;
110
- fieldPath: string;
111
- value: unknown;
112
- onChange: (value: unknown) => void;
113
- addItem: () => void;
114
- removeItem: (index: number) => void;
115
- disabled?: boolean;
116
- rows?: { id: string }[]; // useFieldArray rows for stable rendering
117
- }) => React.ReactNode;
118
- // Optional styling for array layouts (used mainly for 'table')
119
- arrayColors?: {
120
- headerBgClass?: string; // e.g. 'bg-teal-700'
121
- headerTextClass?: string; // e.g. 'text-white'
122
- rowAltBgClass?: string; // e.g. 'bg-teal-50'
123
- };
124
- conditional?: {
125
- field: string;
126
- value: unknown;
127
- }; // For conditional field visibility
128
- hidden?: boolean; // Declarative hide
129
- // Label placement control across inputs
130
- labelPlacement?: 'stacked' | 'inline' | 'hidden';
131
- // Wrapper container className (applies to the field wrapper, not the input)
132
- wrapperClassName?: string;
133
- // Picker-specific optional props (passed through to components when applicable)
134
- minDate?: Date;
135
- maxDate?: Date;
136
- disabledDates?: Array<Date | { from: Date; to: Date }>;
137
- numberOfMonths?: number;
138
- popoverSide?: 'top' | 'right' | 'bottom' | 'left';
139
- showFooter?: boolean;
140
- cancelLabel?: string;
141
- applyLabel?: string;
142
- // Time picker specific
143
- timePrecision?: 'hour' | 'minute' | 'second';
144
- hourCycle?: 12 | 24;
145
- minuteStep?: number;
146
- secondStep?: number;
147
- // File uploader specific options
148
- fileMultiple?: boolean;
149
- fileMaxFiles?: number;
150
- fileAccept?: Accept;
151
- fileLayout?: FileUploaderLayout;
152
- fileWithDownload?: boolean;
153
- fileUploader?: (
154
- file: File,
155
- onProgress: (pct: number) => void,
156
- ) => Promise<Partial<FileRecord>>;
157
- fileOnUploadSuccess?: (file: FileRecord) => void;
158
- fileOnUploadError?: (file: FileRecord, error: unknown) => void;
159
- fileOnRemove?: (file: FileRecord) => void | Promise<void>;
160
- fileOnRetry?: (file: FileRecord) => void;
161
- fileOnRetryAll?: (files: FileRecord[]) => void;
162
- }
163
-
164
- export interface FormBuilderSectionConfig {
165
- id?: string;
166
- title?: string;
167
- description?: string;
168
- fields?: FormBuilderFieldConfig[];
169
- variant?: 'card' | 'separator' | 'plain';
170
- className?: string;
171
- collapsible?: boolean;
172
- defaultCollapsed?: boolean;
173
- layout?: SectionLayout;
174
- grid?: SectionGridOptions;
175
- flex?: SectionFlexOptions;
176
- hidden?: boolean; // Declarative hide
177
- // Tabs layout support: when layout === 'tabs', provide tabs instead of direct fields
178
- tabs?: Array<{
179
- id: string;
180
- label: React.ReactNode;
181
- sections: FormBuilderSectionConfig[];
182
- className?: string;
183
- contentClassName?: string;
184
- }>;
185
- defaultTabId?: string;
186
- tabsListClassName?: string;
187
- tabsContentClassName?: string;
188
- }
11
+ FormBuilderProps,
12
+ FormBuilderFieldConfig,
13
+ FormBuilderSectionConfig,
14
+ } from '../types';
189
15
 
190
- export interface FormBuilderProps {
191
- sections: FormBuilderSectionConfig[];
192
- schema?: z.ZodType<unknown>;
193
- defaultValues?: Record<string, unknown> | null;
194
- onSubmit: (data: unknown) => void | Promise<void>;
195
- onCancel?: () => void;
196
- onReset?: () => void;
197
- onFieldChange?: (
198
- name: string,
199
- value: unknown,
200
- allValues: Record<string, unknown>
201
- ) => void;
202
- submitLabel?: string;
203
- cancelLabel?: string;
204
- resetLabel?: string;
205
- isSubmitting?: boolean;
206
- className?: string;
207
- formClassName?: string;
208
- actionsClassName?: string;
209
- showActions?: boolean;
210
- customActions?: React.ReactNode;
211
- // UI: show a separator line above action buttons
212
- showActionsSeparator?: boolean;
213
- }
214
-
215
- export function FormBuilder({
16
+ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
216
17
  sections,
217
18
  schema,
218
- defaultValues = {},
19
+ defaultValues,
219
20
  onSubmit,
220
21
  onCancel,
221
22
  onReset,
@@ -230,13 +31,14 @@ export function FormBuilder({
230
31
  showActions = true,
231
32
  customActions,
232
33
  showActionsSeparator = true,
233
- }: FormBuilderProps) {
34
+ form,
35
+ }: FormBuilderProps<TFieldValues>) {
234
36
  // Generate schema from field configs if not provided
235
37
  const generatedSchema = useMemo(() => {
236
38
  if (schema) return schema;
237
39
 
238
40
  const generateFieldSchema = (
239
- field: FormBuilderFieldConfig
41
+ field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
240
42
  ): z.ZodType<unknown> => {
241
43
  if (field.validation && field.validation instanceof z.ZodType) {
242
44
  return field.validation;
@@ -430,7 +232,7 @@ export function FormBuilder({
430
232
  case 'object':
431
233
  if (field.fields) {
432
234
  const objectSchema: Record<string, z.ZodType<unknown>> = {};
433
- for (const subField of field.fields) {
235
+ for (const subField of field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>) {
434
236
  objectSchema[subField.name] = generateFieldSchema(subField);
435
237
  }
436
238
  fieldSchema = z.object(objectSchema);
@@ -442,9 +244,9 @@ export function FormBuilder({
442
244
  if (field.fields && field.fields.length > 0) {
443
245
  const arrayItemSchema =
444
246
  field.fields.length === 1
445
- ? generateFieldSchema(field.fields[0])
247
+ ? generateFieldSchema(field.fields[0] as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>)
446
248
  : z.object(
447
- field.fields.reduce((acc, subField) => {
249
+ (field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>).reduce((acc, subField) => {
448
250
  acc[subField.name] = generateFieldSchema(subField);
449
251
  return acc;
450
252
  }, {} as Record<string, z.ZodType<unknown>>)
@@ -463,7 +265,7 @@ export function FormBuilder({
463
265
 
464
266
  const schemaObject: Record<string, z.ZodType<unknown>> = {};
465
267
 
466
- const forEachField = (secs: FormBuilderSectionConfig[]) => {
268
+ const forEachField = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
467
269
  for (const section of secs) {
468
270
  // Traverse tabs if present
469
271
  if (section.tabs && section.tabs.length > 0) {
@@ -472,21 +274,21 @@ export function FormBuilder({
472
274
  }
473
275
  }
474
276
  for (const field of (section.fields ?? [])) {
475
- schemaObject[field.name] = generateFieldSchema(field);
277
+ schemaObject[field.name] = generateFieldSchema(field as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>);
476
278
  }
477
279
  }
478
280
  };
479
281
 
480
282
  forEachField(sections);
481
283
 
482
- return z.object(schemaObject);
284
+ return z.object(schemaObject) as unknown as z.ZodType<TFieldValues>;
483
285
  }, [sections, schema]);
484
286
 
485
287
  // Generate default values from field configs
486
288
  const generatedDefaultValues = useMemo(() => {
487
- const values: Record<string, unknown> = { ...defaultValues };
289
+ const values: Record<string, unknown> = { ...((defaultValues ?? {}) as Record<string, unknown>) };
488
290
 
489
- const processFields = (fields: FormBuilderFieldConfig[]) => {
291
+ const processFields = (fields: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>[]) => {
490
292
  for (const field of fields) {
491
293
  if (
492
294
  values[field.name] === undefined &&
@@ -518,7 +320,7 @@ export function FormBuilder({
518
320
  }
519
321
  };
520
322
 
521
- const forEachSection = (secs: FormBuilderSectionConfig[]) => {
323
+ const forEachSection = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
522
324
  for (const section of secs) {
523
325
  if (section.tabs && section.tabs.length > 0) {
524
326
  for (const tab of section.tabs) {
@@ -534,19 +336,21 @@ export function FormBuilder({
534
336
  return values;
535
337
  }, [sections, defaultValues]);
536
338
 
537
- const form = useForm<FieldValues>({
339
+ const internalForm = useForm<TFieldValues>({
538
340
  // Dynamic schema shape: cast to any to satisfy resolver generics
539
341
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
540
- resolver: zodResolver(generatedSchema as any) as unknown as import('react-hook-form').Resolver<FieldValues, any, FieldValues>,
541
- defaultValues: generatedDefaultValues as FieldValues,
342
+ resolver: zodResolver(generatedSchema as any) as unknown as import('react-hook-form').Resolver<TFieldValues, any, TFieldValues>,
343
+ defaultValues: generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>,
542
344
  });
543
345
 
544
- const { control, handleSubmit, reset, setValue, getValues } = form;
346
+ const activeForm = form ?? internalForm;
347
+
348
+ const { control, handleSubmit, reset, setValue, getValues } = activeForm;
545
349
 
546
350
  // Determine dependency fields to watch
547
351
  const dependencyFields = useMemo(() => {
548
- const set = new Set<string>();
549
- const forEachField = (secs: FormBuilderSectionConfig[]) => {
352
+ const set = new Set<Path<TFieldValues>>();
353
+ const forEachField = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
550
354
  for (const section of secs) {
551
355
  if (section.tabs && section.tabs.length > 0) {
552
356
  for (const tab of section.tabs) {
@@ -586,7 +390,7 @@ export function FormBuilder({
586
390
  );
587
391
 
588
392
  const handleFieldDependencies = useCallback(
589
- (field: FormBuilderFieldConfig) => {
393
+ (field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>) => {
590
394
  if (!hasDependencies || !field.dependencies) return {};
591
395
 
592
396
  const result: { disabled?: boolean; hidden?: boolean } = {};
@@ -610,11 +414,11 @@ export function FormBuilder({
610
414
  break;
611
415
  case 'setValue':
612
416
  if (conditionMet && dep.value !== undefined) {
613
- const currentValue = getValues(field.name);
417
+ const currentValue = getValues(field.name as unknown as Path<TFieldValues>);
614
418
  if (currentValue !== dep.value) {
615
419
  // Defer the update to an effect to prevent state changes during render
616
420
  pendingValueUpdatesRef.current.push({
617
- name: field.name,
421
+ name: field.name as unknown as string,
618
422
  value: dep.value,
619
423
  });
620
424
  }
@@ -638,9 +442,10 @@ export function FormBuilder({
638
442
  }
639
443
  pendingValueUpdatesRef.current = [];
640
444
  for (const [name, value] of updatesMap) {
641
- const current = getValues(name);
445
+ const pathName = name as unknown as Path<TFieldValues>;
446
+ const current = getValues(pathName);
642
447
  if (current !== value) {
643
- setValue(name, value, {
448
+ setValue(pathName, value as unknown as never, {
644
449
  shouldDirty: false,
645
450
  shouldTouch: false,
646
451
  shouldValidate: false,
@@ -651,16 +456,20 @@ export function FormBuilder({
651
456
 
652
457
  // Handle field change with custom onChange
653
458
  const handleFieldChange = useCallback(
654
- (field: FormBuilderFieldConfig, value: unknown) => {
459
+ (
460
+ field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>,
461
+ value: unknown,
462
+ ...extras: unknown[]
463
+ ) => {
655
464
  if (field.onChange) {
656
- field.onChange(value, setValue, getValues);
465
+ field.onChange(value, extras, setValue, getValues);
657
466
  }
658
467
  },
659
468
  [setValue, getValues]
660
469
  );
661
470
 
662
471
  const handleFormSubmit = useCallback(
663
- async (data: unknown) => {
472
+ async (data: TFieldValues) => {
664
473
  try {
665
474
  await onSubmit(data);
666
475
  } catch (error) {
@@ -671,154 +480,89 @@ export function FormBuilder({
671
480
  );
672
481
 
673
482
  const handleReset = useCallback(() => {
674
- reset(generatedDefaultValues);
483
+ reset(generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>);
675
484
  onReset?.();
676
485
  }, [reset, generatedDefaultValues, onReset]);
677
486
 
678
487
  // Build SectionBuilder nodes from form sections/fields
679
- const sectionNodes: SectionNode[] = useMemo(() => {
680
- const buildLeavesFromFields = (fields?: FormBuilderFieldConfig[]): SectionNode['children'] =>
681
- (fields ?? [])
682
- .map((field) => {
683
- const fieldState = handleFieldDependencies(field);
684
- if (field.hidden || fieldState.hidden) return null;
685
-
686
- const spanMd = Math.max(1, Math.min(12, field.gridCols ?? 1));
687
-
688
- return {
689
- key: field.name,
690
- span: { base: 1, md: spanMd },
691
- className: field.wrapperClassName,
692
- hidden: field.hidden,
693
- content: (
694
- <FormBuilderField
695
- key={field.name}
696
- field={{
697
- ...field,
698
- disabled: field.disabled || fieldState.disabled,
699
- }}
700
- control={control}
701
- onChange={(value) => {
702
- handleFieldChange(field, value);
703
- onFieldChange?.(field.name, value, getValues());
704
- }}
705
- onFieldChange={onFieldChange}
706
- />
707
- ),
708
- };
709
- })
710
- .filter(Boolean) as SectionNode['children'];
711
-
712
- const buildSectionNode = (
713
- section: FormBuilderSectionConfig,
714
- sectionIndex: number,
715
- ): SectionNode => {
716
- const baseNode: SectionNode = {
717
- id: section.id ?? `section-${sectionIndex}`,
718
- title: section.title,
719
- subtitle: section.description,
720
- variant: section.variant ?? 'plain',
721
- className: section.className,
722
- layout: section.layout ?? (section.tabs && section.tabs.length > 0 ? 'tabs' : 'grid'),
723
- grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
724
- flex: section.flex,
725
- hidden: section.hidden,
726
- };
727
-
728
- // Tabs layout
729
- if (baseNode.layout === 'tabs' && section.tabs && section.tabs.length > 0) {
730
- baseNode.defaultTabId = section.defaultTabId ?? section.tabs[0]?.id;
731
- baseNode.tabsListClassName = section.tabsListClassName;
732
- baseNode.tabsContentClassName = section.tabsContentClassName;
733
- baseNode.tabs = section.tabs.map((tab, _tabIdx) => {
734
- // Each tab can contain multiple sub-sections; wrap them under a container node
735
- const nestedNodes = tab.sections.map((subSection, subIdx) => buildSectionNode(subSection, subIdx));
736
- const containerNode: SectionNode = {
737
- id: `${baseNode.id}-tab-${tab.id}`,
738
- title: undefined,
739
- subtitle: undefined,
740
- variant: 'plain',
741
- layout: 'grid',
742
- grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
743
- children: nestedNodes,
744
- } as SectionNode;
745
- return {
746
- id: tab.id,
747
- label: tab.label,
748
- className: tab.className,
749
- contentClassName: tab.contentClassName,
750
- node: containerNode,
751
- };
752
- });
753
- return baseNode;
754
- }
755
-
756
- // Regular non-tab section with direct fields
757
- baseNode.children = buildLeavesFromFields(section.fields);
758
- return baseNode;
759
- };
488
+ const sectionNodes = useMemo(
489
+ () =>
490
+ buildSectionNodes({
491
+ sections,
492
+ control,
493
+ handleFieldDependencies,
494
+ handleFieldChange,
495
+ onFieldChange,
496
+ getValues,
497
+ }),
498
+ [sections, control, handleFieldDependencies, handleFieldChange, onFieldChange, getValues],
499
+ );
760
500
 
761
- return sections.map((section, sectionIndex) => buildSectionNode(section, sectionIndex));
762
- }, [
763
- sections,
764
- control,
765
- handleFieldDependencies,
766
- handleFieldChange,
767
- onFieldChange,
768
- getValues,
769
- ]);
501
+ const contextValue = useMemo(
502
+ () => ({
503
+ control,
504
+ getValues,
505
+ setValue,
506
+ onFieldChange,
507
+ handleFieldDependencies,
508
+ handleFieldChange,
509
+ }) satisfies FormBuilderContextValue<TFieldValues>,
510
+ [control, getValues, setValue, onFieldChange, handleFieldDependencies, handleFieldChange],
511
+ );
770
512
 
771
513
  return (
772
- <div className={cn('space-y-6', className)}>
773
- <form
774
- onSubmit={handleSubmit(handleFormSubmit)}
775
- className={cn('space-y-6', formClassName)}
776
- >
777
- <SectionBuilder sections={sectionNodes} />
778
-
779
- {showActions && (
780
- <div
781
- className={cn(
782
- 'flex flex-col sm:flex-row gap-3',
783
- showActionsSeparator && 'pt-6',
784
- showActionsSeparator && 'border-t',
785
- actionsClassName
786
- )}
787
- >
788
- <Button
789
- type="submit"
790
- disabled={isSubmitting}
791
- className="sm:order-last"
514
+ <FormBuilderContext.Provider value={contextValue as unknown as FormBuilderContextValue<FieldValues>}>
515
+ <div className={cn('space-y-6', className)}>
516
+ <form
517
+ onSubmit={handleSubmit(handleFormSubmit)}
518
+ className={cn('space-y-6', formClassName)}
519
+ >
520
+ <SectionBuilder sections={sectionNodes} />
521
+
522
+ {showActions && (
523
+ <div
524
+ className={cn(
525
+ 'flex flex-col sm:flex-row gap-3',
526
+ showActionsSeparator && 'pt-6',
527
+ showActionsSeparator && 'border-t',
528
+ actionsClassName
529
+ )}
792
530
  >
793
- {isSubmitting ? 'Submitting...' : submitLabel}
794
- </Button>
795
-
796
- {onCancel && (
797
- <Button
798
- type="button"
799
- variant="outline"
800
- onClick={onCancel}
801
- disabled={isSubmitting}
802
- >
803
- {cancelLabel}
804
- </Button>
805
- )}
806
-
807
- {onReset && (
808
531
  <Button
809
- type="button"
810
- variant="outline"
811
- onClick={handleReset}
532
+ type="submit"
812
533
  disabled={isSubmitting}
534
+ className="sm:order-last"
813
535
  >
814
- {resetLabel}
536
+ {isSubmitting ? 'Submitting...' : submitLabel}
815
537
  </Button>
816
- )}
817
538
 
818
- {customActions}
819
- </div>
820
- )}
821
- </form>
822
- </div>
539
+ {onCancel && (
540
+ <Button
541
+ type="button"
542
+ variant="outline"
543
+ onClick={onCancel}
544
+ disabled={isSubmitting}
545
+ >
546
+ {cancelLabel}
547
+ </Button>
548
+ )}
549
+
550
+ {onReset && (
551
+ <Button
552
+ type="button"
553
+ variant="outline"
554
+ onClick={handleReset}
555
+ disabled={isSubmitting}
556
+ >
557
+ {resetLabel}
558
+ </Button>
559
+ )}
560
+
561
+ {customActions}
562
+ </div>
563
+ )}
564
+ </form>
565
+ </div>
566
+ </FormBuilderContext.Provider>
823
567
  );
824
568
  }
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type {
3
+ Control,
4
+ FieldValues,
5
+ Path,
6
+ UseFormGetValues,
7
+ UseFormSetValue,
8
+ } from 'react-hook-form';
9
+ import type { FormBuilderFieldConfig } from '../types';
10
+
11
+ interface DependencyState {
12
+ disabled?: boolean;
13
+ hidden?: boolean;
14
+ }
15
+
16
+ export interface FormBuilderContextValue<TFieldValues extends FieldValues = FieldValues> {
17
+ control: Control<TFieldValues>;
18
+ getValues: UseFormGetValues<TFieldValues>;
19
+ setValue: UseFormSetValue<TFieldValues>;
20
+ onFieldChange?: (
21
+ name: Path<TFieldValues> | string,
22
+ value: unknown,
23
+ allValues: TFieldValues
24
+ ) => void;
25
+ handleFieldDependencies: (
26
+ field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
27
+ ) => DependencyState;
28
+ handleFieldChange: (
29
+ field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>,
30
+ value: unknown,
31
+ ...extras: unknown[]
32
+ ) => void;
33
+ }
34
+
35
+ const FormBuilderContext = createContext<FormBuilderContextValue<FieldValues> | null>(null);
36
+
37
+ export function useFormBuilderContext<TFieldValues extends FieldValues = FieldValues>() {
38
+ const value = useContext(FormBuilderContext) as FormBuilderContextValue<TFieldValues> | null;
39
+ if (!value) {
40
+ throw new Error('FormBuilderGroup must be used within a FormBuilder.');
41
+ }
42
+ return value;
43
+ }
44
+
45
+ export { FormBuilderContext };