@k3-universe/react-kit 0.0.26 → 0.0.27

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 (29) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +15979 -15536
  4. package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
  5. package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
  6. package/dist/kit/builder/form/hooks/useFormBuilder.d.ts +15 -0
  7. package/dist/kit/builder/form/hooks/useFormBuilder.d.ts.map +1 -0
  8. package/dist/kit/builder/form/index.d.ts +1 -0
  9. package/dist/kit/builder/form/index.d.ts.map +1 -1
  10. package/dist/kit/builder/form/types.d.ts +1 -1
  11. package/dist/kit/builder/form/types.d.ts.map +1 -1
  12. package/dist/kit/components/forminfo/FormInfoError.d.ts +12 -0
  13. package/dist/kit/components/forminfo/FormInfoError.d.ts.map +1 -0
  14. package/dist/kit/components/forminfo/index.d.ts +2 -0
  15. package/dist/kit/components/forminfo/index.d.ts.map +1 -0
  16. package/dist/kit/themes/base.css +1 -1
  17. package/dist/kit/themes/clean-slate.css +11 -5
  18. package/dist/kit/themes/default.css +11 -5
  19. package/dist/kit/themes/minimal-modern.css +11 -5
  20. package/dist/kit/themes/spotify.css +11 -5
  21. package/package.json +11 -8
  22. package/src/index.ts +1 -0
  23. package/src/kit/builder/form/components/FormBuilder.tsx +51 -332
  24. package/src/kit/builder/form/components/FormBuilderField.tsx +12 -4
  25. package/src/kit/builder/form/hooks/useFormBuilder.ts +399 -0
  26. package/src/kit/builder/form/index.ts +1 -0
  27. package/src/kit/builder/form/types.ts +8 -1
  28. package/src/kit/components/forminfo/FormInfoError.tsx +120 -0
  29. package/src/kit/components/forminfo/index.ts +1 -0
@@ -1,17 +1,23 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'react';
2
- import { useForm, useWatch, type FieldValues, type Path } from 'react-hook-form';
3
- import { zodResolver } from '@hookform/resolvers/zod';
4
- import { z } from 'zod';
2
+ import {
3
+ useWatch,
4
+ type FieldValues,
5
+ type Path,
6
+ } from 'react-hook-form';
5
7
  import { cn } from '../../../../shadcn/lib/utils';
6
8
  import { Button } from '../../../../shadcn/ui/button';
7
9
  import SectionBuilder from '../../section/SectionBuilder';
8
10
  import { buildSectionNodes } from './sectionNodes';
9
- import { FormBuilderContext, type FormBuilderContextValue } from './FormBuilderContext';
11
+ import {
12
+ FormBuilderContext,
13
+ type FormBuilderContextValue,
14
+ } from './FormBuilderContext';
10
15
  import type {
11
16
  FormBuilderProps,
12
17
  FormBuilderFieldConfig,
13
18
  FormBuilderSectionConfig,
14
19
  } from '../types';
20
+ import { useFormBuilder } from '../hooks/useFormBuilder';
15
21
 
16
22
  export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
17
23
  sections,
@@ -33,323 +39,15 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
33
39
  showActionsSeparator = true,
34
40
  form,
35
41
  }: FormBuilderProps<TFieldValues>) {
36
- // Generate schema from field configs if not provided
37
- const generatedSchema = useMemo(() => {
38
- if (schema) return schema;
39
-
40
- const generateFieldSchema = (
41
- field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
42
- ): z.ZodType<unknown> => {
43
- if (field.validation && field.validation instanceof z.ZodType) {
44
- return field.validation;
45
- }
46
-
47
- // Handle validation object format
48
- if (
49
- field.validation &&
50
- typeof field.validation === 'object' &&
51
- !(field.validation instanceof z.ZodType)
52
- ) {
53
- const validationObj = field.validation;
54
- let baseSchema: z.ZodType<unknown>;
55
-
56
- // Determine base schema type
57
- switch (field.type) {
58
- case 'email':
59
- baseSchema = z.string().email('Invalid email address');
60
- break;
61
- case 'number':
62
- baseSchema = z.number();
63
- break;
64
- case 'file':
65
- baseSchema = z.array(z.unknown());
66
- break;
67
- case 'date_picker':
68
- case 'month':
69
- case 'date':
70
- case 'time':
71
- case 'date_time':
72
- baseSchema = z.date();
73
- break;
74
- case 'date_range':
75
- case 'time_range':
76
- case 'date_time_range':
77
- baseSchema = z
78
- .object({ from: z.date().optional().nullable(), to: z.date().optional().nullable() })
79
- .nullable();
80
- break;
81
- case 'month_range':
82
- baseSchema = z
83
- .object({ start: z.date().optional().nullable(), end: z.date().optional().nullable() })
84
- .nullable();
85
- break;
86
- case 'autocomplete': {
87
- const single = z
88
- .union([z.string(), z.number(), z.object({})])
89
- .nullable();
90
- const multi = z.array(
91
- z.union([z.string(), z.number(), z.object({})])
92
- );
93
- baseSchema = field.multiple ? multi : single;
94
- break;
95
- }
96
- case 'checkbox':
97
- case 'switch':
98
- baseSchema = z.boolean();
99
- break;
100
- case 'custom_field':
101
- baseSchema = z.any();
102
- break;
103
- default:
104
- baseSchema = z.string();
105
- }
106
-
107
- // Apply validation constraints
108
- if (validationObj.pattern && baseSchema instanceof z.ZodString) {
109
- baseSchema = baseSchema.regex(
110
- validationObj.pattern.value,
111
- validationObj.pattern.message
112
- );
113
- }
114
- if (validationObj.min && baseSchema instanceof z.ZodNumber) {
115
- baseSchema = baseSchema.min(
116
- validationObj.min.value,
117
- validationObj.min.message
118
- );
119
- }
120
- if (validationObj.max && baseSchema instanceof z.ZodNumber) {
121
- baseSchema = baseSchema.max(
122
- validationObj.max.value,
123
- validationObj.max.message
124
- );
125
- }
126
- if (validationObj.minLength && baseSchema instanceof z.ZodString) {
127
- baseSchema = baseSchema.min(
128
- validationObj.minLength.value,
129
- validationObj.minLength.message
130
- );
131
- }
132
- if (validationObj.maxLength && baseSchema instanceof z.ZodString) {
133
- baseSchema = baseSchema.max(
134
- validationObj.maxLength.value,
135
- validationObj.maxLength.message
136
- );
137
- }
138
- // Array item count constraints
139
- if (baseSchema instanceof z.ZodArray) {
140
- let arr = baseSchema as z.ZodArray<z.ZodTypeAny>;
141
- if (validationObj.minItems) {
142
- arr = arr.min(
143
- validationObj.minItems.value,
144
- validationObj.minItems.message,
145
- );
146
- }
147
- if (validationObj.maxItems) {
148
- arr = arr.max(
149
- validationObj.maxItems.value,
150
- validationObj.maxItems.message,
151
- );
152
- }
153
- // If required and file field, enforce at least 1 item when no explicit minItems
154
- if (field.type === 'file' && field.required && !validationObj.minItems) {
155
- arr = arr.min(1, `${field.label} requires at least 1 file`);
156
- }
157
- baseSchema = arr;
158
- }
159
-
160
- return field.required ? baseSchema : baseSchema.optional();
161
- }
162
-
163
- let fieldSchema: z.ZodType<unknown>;
164
-
165
- switch (field.type) {
166
- case 'email':
167
- fieldSchema = z.string().email('Invalid email address');
168
- break;
169
- case 'number':
170
- fieldSchema = z.number();
171
- break;
172
- case 'file': {
173
- let arr = z.array(z.unknown());
174
- // If required, ensure at least 1 file
175
- if (field.required) {
176
- arr = arr.min(1, `${field.label} requires at least 1 file`);
177
- }
178
- fieldSchema = arr;
179
- break;
180
- }
181
- case 'date_picker':
182
- case 'month':
183
- case 'date':
184
- case 'time':
185
- case 'date_time':
186
- fieldSchema = z.date();
187
- break;
188
- case 'date_range':
189
- case 'time_range':
190
- case 'date_time_range':
191
- fieldSchema = z
192
- .object({ from: z.date().optional().nullable(), to: z.date().optional().nullable() })
193
- .nullable();
194
- break;
195
- case 'month_range':
196
- fieldSchema = z
197
- .object({ start: z.date().optional().nullable(), end: z.date().optional().nullable() })
198
- .nullable();
199
- break;
200
- case 'autocomplete': {
201
- const single = z
202
- .union([z.string(), z.number(), z.object({})])
203
- .nullable();
204
- const multi = z.array(
205
- z.union([z.string(), z.number(), z.object({})])
206
- );
207
- fieldSchema = field.multiple ? multi : single;
208
- break;
209
- }
210
- case 'checkbox':
211
- case 'switch':
212
- fieldSchema = z.boolean();
213
- break;
214
- case 'select':
215
- case 'radio':
216
- if (field.options && field.options.length > 0) {
217
- // Build a union of literals to allow specific values, including null if present
218
- const literals: Array<z.ZodLiteral<string | number | null>> = field.options.map((opt) =>
219
- z.literal(opt.value as string | number | null)
220
- );
221
- if (literals.length === 1) {
222
- fieldSchema = literals[0];
223
- } else {
224
- fieldSchema = z.union(
225
- literals as [
226
- z.ZodLiteral<string | number | null>,
227
- ...z.ZodLiteral<string | number | null>[]
228
- ]
229
- );
230
- }
231
- } else {
232
- fieldSchema = z.string();
233
- }
234
- break;
235
- case 'object':
236
- if (field.fields) {
237
- const objectSchema: Record<string, z.ZodType<unknown>> = {};
238
- for (const subField of field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>) {
239
- objectSchema[subField.name] = generateFieldSchema(subField);
240
- }
241
- fieldSchema = z.object(objectSchema);
242
- } else {
243
- fieldSchema = z.object({});
244
- }
245
- break;
246
- case 'array':
247
- if (field.fields && field.fields.length > 0) {
248
- const arrayItemSchema =
249
- field.fields.length === 1
250
- ? generateFieldSchema(field.fields[0] as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>)
251
- : z.object(
252
- (field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>).reduce((acc, subField) => {
253
- acc[subField.name] = generateFieldSchema(subField);
254
- return acc;
255
- }, {} as Record<string, z.ZodType<unknown>>)
256
- );
257
- fieldSchema = z.array(arrayItemSchema);
258
- } else {
259
- fieldSchema = z.array(z.unknown());
260
- }
261
- break;
262
- case 'custom_field':
263
- fieldSchema = z.any();
264
- break;
265
- default:
266
- fieldSchema = z.string();
267
- }
268
-
269
- return field.required ? fieldSchema : fieldSchema.optional();
270
- };
271
-
272
- const schemaObject: Record<string, z.ZodType<unknown>> = {};
273
-
274
- const forEachField = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
275
- for (const section of secs) {
276
- // Traverse tabs if present
277
- if (section.tabs && section.tabs.length > 0) {
278
- for (const tab of section.tabs) {
279
- forEachField(tab.sections);
280
- }
281
- }
282
- for (const field of (section.fields ?? [])) {
283
- schemaObject[field.name] = generateFieldSchema(field as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>);
284
- }
285
- }
286
- };
287
-
288
- forEachField(sections);
289
-
290
- return z.object(schemaObject) as unknown as z.ZodType<TFieldValues>;
291
- }, [sections, schema]);
292
-
293
- // Generate default values from field configs
294
- const generatedDefaultValues = useMemo(() => {
295
- const values: Record<string, unknown> = { ...((defaultValues ?? {}) as Record<string, unknown>) };
296
-
297
- const processFields = (fields: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>[]) => {
298
- for (const field of fields) {
299
- if (
300
- values[field.name] === undefined &&
301
- field.defaultValue !== undefined
302
- ) {
303
- values[field.name] = field.defaultValue;
304
- }
305
-
306
- if (field.type === 'object' && field.fields) {
307
- if (!values[field.name]) values[field.name] = {};
308
- const nestedValues: Record<string, unknown> = {};
309
- for (const subField of field.fields) {
310
- if (subField.defaultValue !== undefined) {
311
- nestedValues[subField.name] = subField.defaultValue;
312
- }
313
- }
314
- const existing =
315
- values[field.name] && typeof values[field.name] === 'object'
316
- ? (values[field.name] as Record<string, unknown>)
317
- : {};
318
- values[field.name] = { ...nestedValues, ...existing };
319
- }
320
-
321
- if (field.type === 'array' && field.fields) {
322
- if (!values[field.name]) {
323
- values[field.name] = field.defaultValue || [];
324
- }
325
- }
326
- }
327
- };
328
-
329
- const forEachSection = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
330
- for (const section of secs) {
331
- if (section.tabs && section.tabs.length > 0) {
332
- for (const tab of section.tabs) {
333
- forEachSection(tab.sections);
334
- }
335
- }
336
- processFields(section.fields ?? []);
337
- }
338
- };
339
-
340
- forEachSection(sections);
341
-
342
- return values;
343
- }, [sections, defaultValues]);
344
-
345
- const internalForm = useForm<TFieldValues>({
346
- // Dynamic schema shape: cast to any to satisfy resolver generics
347
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
348
- resolver: zodResolver(generatedSchema as any) as unknown as import('react-hook-form').Resolver<TFieldValues, any, TFieldValues>,
349
- defaultValues: generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>,
42
+ // Always call useFormBuilder hook (hooks must be called unconditionally)
43
+ const { form: generatedForm } = useFormBuilder({
44
+ sections,
45
+ schema,
46
+ defaultValues: defaultValues ?? undefined,
350
47
  });
351
48
 
352
- const activeForm = form ?? internalForm;
49
+ // Use provided form or fall back to generated form
50
+ const activeForm = form ?? generatedForm;
353
51
 
354
52
  const { control, handleSubmit, reset, setValue, getValues } = activeForm;
355
53
 
@@ -363,7 +61,7 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
363
61
  forEachField(tab.sections);
364
62
  }
365
63
  }
366
- for (const f of (section.fields ?? [])) {
64
+ for (const f of section.fields ?? []) {
367
65
  for (const d of f.dependencies || []) {
368
66
  set.add(d.field);
369
67
  }
@@ -391,12 +89,14 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
391
89
 
392
90
  // Handle field dependencies
393
91
  // Queue dependency-driven value updates to avoid calling setValue during render
394
- const pendingValueUpdatesRef = useRef<Array<{ name: string; value: unknown }>>(
395
- []
396
- );
92
+ const pendingValueUpdatesRef = useRef<
93
+ Array<{ name: string; value: unknown }>
94
+ >([]);
397
95
 
398
96
  const handleFieldDependencies = useCallback(
399
- (field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>) => {
97
+ (
98
+ field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
99
+ ) => {
400
100
  if (!hasDependencies || !field.dependencies) return {};
401
101
 
402
102
  const result: { disabled?: boolean; hidden?: boolean } = {};
@@ -420,7 +120,9 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
420
120
  break;
421
121
  case 'setValue':
422
122
  if (conditionMet && dep.value !== undefined) {
423
- const currentValue = getValues(field.name as unknown as Path<TFieldValues>);
123
+ const currentValue = getValues(
124
+ field.name as unknown as Path<TFieldValues>
125
+ );
424
126
  if (currentValue !== dep.value) {
425
127
  // Defer the update to an effect to prevent state changes during render
426
128
  pendingValueUpdatesRef.current.push({
@@ -486,9 +188,9 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
486
188
  );
487
189
 
488
190
  const handleReset = useCallback(() => {
489
- reset(generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>);
191
+ reset();
490
192
  onReset?.();
491
- }, [reset, generatedDefaultValues, onReset]);
193
+ }, [reset, onReset]);
492
194
 
493
195
  // Build SectionBuilder nodes from form sections/fields
494
196
  const sectionNodes = useMemo(
@@ -501,23 +203,40 @@ export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
501
203
  onFieldChange,
502
204
  getValues,
503
205
  }),
504
- [sections, control, handleFieldDependencies, handleFieldChange, onFieldChange, getValues],
206
+ [
207
+ sections,
208
+ control,
209
+ handleFieldDependencies,
210
+ handleFieldChange,
211
+ onFieldChange,
212
+ getValues,
213
+ ]
505
214
  );
506
215
 
507
216
  const contextValue = useMemo(
508
- () => ({
217
+ () =>
218
+ ({
219
+ control,
220
+ getValues,
221
+ setValue,
222
+ onFieldChange,
223
+ handleFieldDependencies,
224
+ handleFieldChange,
225
+ } satisfies FormBuilderContextValue<TFieldValues>),
226
+ [
509
227
  control,
510
228
  getValues,
511
229
  setValue,
512
230
  onFieldChange,
513
231
  handleFieldDependencies,
514
232
  handleFieldChange,
515
- }) satisfies FormBuilderContextValue<TFieldValues>,
516
- [control, getValues, setValue, onFieldChange, handleFieldDependencies, handleFieldChange],
233
+ ]
517
234
  );
518
235
 
519
236
  return (
520
- <FormBuilderContext.Provider value={contextValue as unknown as FormBuilderContextValue<FieldValues>}>
237
+ <FormBuilderContext.Provider
238
+ value={contextValue as unknown as FormBuilderContextValue<FieldValues>}
239
+ >
521
240
  <div className={cn('space-y-6', className)}>
522
241
  <form
523
242
  onSubmit={handleSubmit(handleFormSubmit)}
@@ -335,7 +335,9 @@ export function FormBuilderField<
335
335
  <p className="text-sm text-muted-foreground">{field.description}</p>
336
336
  )}
337
337
  {error && (
338
- <p className="text-sm text-destructive">{error.message}</p>
338
+ <p className="text-sm font-medium text-destructive" role="alert" aria-live="polite">
339
+ {error.message}
340
+ </p>
339
341
  )}
340
342
  </div>
341
343
  );
@@ -351,7 +353,9 @@ export function FormBuilderField<
351
353
  <p className="text-sm text-muted-foreground">{field.description}</p>
352
354
  )}
353
355
  {error && (
354
- <p className="text-sm text-destructive">{error.message}</p>
356
+ <p className="text-sm font-medium text-destructive" role="alert" aria-live="polite">
357
+ {error.message}
358
+ </p>
355
359
  )}
356
360
  </div>
357
361
  );
@@ -371,7 +375,9 @@ export function FormBuilderField<
371
375
  <p className="text-sm text-muted-foreground">{field.description}</p>
372
376
  )}
373
377
  {error && (
374
- <p className="text-sm text-destructive">{error.message}</p>
378
+ <p className="text-sm font-medium text-destructive" role="alert" aria-live="polite">
379
+ {error.message}
380
+ </p>
375
381
  )}
376
382
  </div>
377
383
  );
@@ -389,7 +395,9 @@ export function FormBuilderField<
389
395
  <p className="text-sm text-muted-foreground">{field.description}</p>
390
396
  )}
391
397
  {error && (
392
- <p className="text-sm text-destructive">{error.message}</p>
398
+ <p className="text-sm font-medium text-destructive" role="alert" aria-live="polite">
399
+ {error.message}
400
+ </p>
393
401
  )}
394
402
  </div>
395
403
  );