@classytic/formkit 1.0.3 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @classytic/formkit
2
2
 
3
- Headless, type-safe form generation engine for React 18/19. Schema-driven with full TypeScript support.
3
+ Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@classytic/formkit.svg)](https://www.npmjs.com/package/@classytic/formkit)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -8,14 +8,24 @@ Headless, type-safe form generation engine for React 18/19. Schema-driven with f
8
8
 
9
9
  ## Features
10
10
 
11
- - ðŸŽŊ **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
12
- - 📝 **Schema-driven** - Define forms with JSON/TypeScript schemas
13
- - 🔒 **Type-safe** - Full TypeScript support with generics
14
- - ⚡ **React Hook Form** - Built on top of the best form library
15
- - ðŸŽĻ **Variants** - Support for multiple component variants
16
- - 🔀 **Conditional fields** - Show/hide fields based on form values
17
- - ðŸ“ą **Responsive layouts** - Multi-column grid layouts
18
- - ðŸŠķ **Lightweight** - ~6KB minified, tree-shakeable
11
+ - **Minimal boilerplate** - `useFormKit` hook: 5 lines to set up a complete form
12
+ - **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
13
+ - **Schema-driven** - Define forms with JSON/TypeScript schemas, defaults extracted automatically
14
+ - **Type-safe** - Full TypeScript support with generics
15
+ - **React Hook Form** - Built on top of the best form library, referentially stable return values
16
+ - **React 19** - Uses modern React 19 patterns (Context as provider, ref as prop)
17
+ - **Server Components** - Dedicated `@classytic/formkit/server` entry point for RSC
18
+ - **Variants** - Support for multiple component variants
19
+ - **Conditional fields** - Show/hide fields based on form values (function, DSL rules, AND/OR logic)
20
+ - **Responsive layouts** - Multi-column grid layouts
21
+ - **Accessibility** - Auto-generated `fieldId`, `error`, and `fieldState` props
22
+ - **Validation helpers** - `buildValidationRules` generates RHF rules from schema props
23
+ - **Lightweight** - ~7KB gzipped, tree-shakeable
24
+
25
+ ## Requirements
26
+
27
+ - **React 19.0+** (React 18 is not supported)
28
+ - **React Hook Form 7.55.0+**
19
29
 
20
30
  ## Installation
21
31
 
@@ -31,7 +41,7 @@ yarn add @classytic/formkit react-hook-form
31
41
 
32
42
  ### 1. Create Field Components
33
43
 
34
- Each field component wraps your UI library with `react-hook-form`'s Controller:
44
+ Each field component receives `FieldComponentProps` including `error`, `fieldId`, and the full `field` config:
35
45
 
36
46
  ```tsx
37
47
  // components/form/form-input.tsx
@@ -42,23 +52,29 @@ import type { FieldComponentProps } from "@classytic/formkit";
42
52
  import { Input } from "@/components/ui/input";
43
53
  import { Label } from "@/components/ui/label";
44
54
 
45
- export function FormInput({ control, name, label, placeholder, required }: FieldComponentProps) {
55
+ export function FormInput({
56
+ control,
57
+ field,
58
+ label,
59
+ placeholder,
60
+ required,
61
+ error,
62
+ fieldId,
63
+ }: FieldComponentProps) {
46
64
  return (
47
65
  <Controller
48
- name={name}
66
+ name={field.name}
49
67
  control={control}
50
- render={({ field, fieldState }) => (
68
+ render={({ field: rhfField }) => (
51
69
  <div className="space-y-2">
52
70
  {label && (
53
- <Label htmlFor={name}>
71
+ <Label htmlFor={fieldId}>
54
72
  {label}
55
73
  {required && <span className="text-red-500 ml-1">*</span>}
56
74
  </Label>
57
75
  )}
58
- <Input {...field} id={name} placeholder={placeholder} />
59
- {fieldState.error && (
60
- <p className="text-sm text-red-500">{fieldState.error.message}</p>
61
- )}
76
+ <Input {...rhfField} id={fieldId} placeholder={placeholder} />
77
+ {error && <p className="text-sm text-red-500">{error.message}</p>}
62
78
  </div>
63
79
  )}
64
80
  />
@@ -74,7 +90,11 @@ Register your components and layouts:
74
90
  // lib/form-adapter.tsx
75
91
  "use client";
76
92
 
77
- import { FormSystemProvider, type ComponentRegistry, type LayoutRegistry } from "@classytic/formkit";
93
+ import {
94
+ FormSystemProvider,
95
+ type ComponentRegistry,
96
+ type LayoutRegistry,
97
+ } from "@classytic/formkit";
78
98
  import { FormInput } from "@/components/form/form-input";
79
99
 
80
100
  const components: ComponentRegistry = {
@@ -112,13 +132,11 @@ export function FormProvider({ children }: { children: React.ReactNode }) {
112
132
  // app/signup/page.tsx
113
133
  "use client";
114
134
 
115
- import { useForm } from "react-hook-form";
116
135
  import { zodResolver } from "@hookform/resolvers/zod";
117
136
  import { z } from "zod";
118
- import { FormGenerator, type FormSchema } from "@classytic/formkit";
137
+ import { FormGenerator, useFormKit, type FormSchema } from "@classytic/formkit";
119
138
  import { FormProvider } from "@/lib/form-adapter";
120
139
 
121
- // Validation schema
122
140
  const signupSchema = z.object({
123
141
  firstName: z.string().min(2),
124
142
  lastName: z.string().min(2),
@@ -128,36 +146,60 @@ const signupSchema = z.object({
128
146
 
129
147
  type SignupData = z.infer<typeof signupSchema>;
130
148
 
131
- // Form schema (type-safe!)
132
149
  const formSchema: FormSchema<SignupData> = {
133
150
  sections: [
134
151
  {
135
152
  title: "Personal Information",
136
153
  cols: 2,
137
154
  fields: [
138
- { name: "firstName", type: "text", label: "First Name", required: true },
139
- { name: "lastName", type: "text", label: "Last Name", required: true },
155
+ {
156
+ name: "firstName",
157
+ type: "text",
158
+ label: "First Name",
159
+ required: true,
160
+ defaultValue: "",
161
+ },
162
+ {
163
+ name: "lastName",
164
+ type: "text",
165
+ label: "Last Name",
166
+ required: true,
167
+ defaultValue: "",
168
+ },
140
169
  ],
141
170
  },
142
171
  {
143
172
  title: "Account",
144
173
  fields: [
145
- { name: "email", type: "email", label: "Email", required: true },
146
- { name: "password", type: "password", label: "Password", required: true },
174
+ {
175
+ name: "email",
176
+ type: "email",
177
+ label: "Email",
178
+ required: true,
179
+ defaultValue: "",
180
+ },
181
+ {
182
+ name: "password",
183
+ type: "password",
184
+ label: "Password",
185
+ required: true,
186
+ defaultValue: "",
187
+ },
147
188
  ],
148
189
  },
149
190
  ],
150
191
  };
151
192
 
152
193
  export default function SignupPage() {
153
- const form = useForm<SignupData>({
194
+ const { handleSubmit, generatorProps } = useFormKit({
195
+ schema: formSchema,
154
196
  resolver: zodResolver(signupSchema),
155
197
  });
156
198
 
157
199
  return (
158
200
  <FormProvider>
159
- <form onSubmit={form.handleSubmit(console.log)} className="space-y-8">
160
- <FormGenerator schema={formSchema} control={form.control} />
201
+ <form onSubmit={handleSubmit(console.log)} className="space-y-8">
202
+ <FormGenerator {...generatorProps} />
161
203
  <button type="submit">Sign Up</button>
162
204
  </form>
163
205
  </FormProvider>
@@ -167,67 +209,125 @@ export default function SignupPage() {
167
209
 
168
210
  ## API Reference
169
211
 
212
+ ### useFormKit
213
+
214
+ Convenience hook that combines schema default extraction with react-hook-form setup. Returns all `useForm` methods plus ready-to-spread `generatorProps`.
215
+
216
+ **Referentially stable** — the return value preserves the original `useForm` object identity across re-renders, so it's safe to use in `useEffect` dependency arrays.
217
+
218
+ ```tsx
219
+ import { useFormKit, FormGenerator } from "@classytic/formkit";
220
+
221
+ const form = useFormKit({
222
+ schema: formSchema,
223
+ resolver: zodResolver(validationSchema), // optional
224
+ defaultValues: { email: "pre@fill.com" }, // optional overrides
225
+ disabled: false, // optional
226
+ variant: "compact", // optional
227
+ className: "my-form", // optional
228
+ mode: "onBlur", // any useForm option
229
+ });
230
+
231
+ const { handleSubmit, generatorProps } = form;
232
+
233
+ // Safe to use in useEffect deps — form is referentially stable
234
+ useEffect(() => {
235
+ if (open) form.reset(defaults);
236
+ }, [open, form]);
237
+
238
+ return (
239
+ <form onSubmit={handleSubmit(onSubmit)}>
240
+ <FormGenerator {...generatorProps} />
241
+ <button type="submit">Submit</button>
242
+ </form>
243
+ );
244
+ ```
245
+
246
+ Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority).
247
+
248
+ `generatorProps` is memoized — it only recomputes when `schema`, `control`, `disabled`, `variant`, or `className` change.
249
+
170
250
  ### FormGenerator
171
251
 
172
- The main component that renders forms from a schema.
252
+ The main component that renders forms from a schema. Supports React 19 `ref` as a regular prop.
173
253
 
174
254
  ```tsx
175
255
  <FormGenerator
176
- schema={formSchema} // Required: Form schema
177
- control={form.control} // Required: React Hook Form control
178
- disabled={false} // Optional: Disable all fields
179
- variant="default" // Optional: Global variant
180
- className="my-form" // Optional: Root element class
256
+ schema={formSchema} // Required: Form schema
257
+ control={form.control} // Optional: React Hook Form control (or wrap in <FormProvider>)
258
+ disabled={false} // Optional: Disable all fields
259
+ variant="default" // Optional: Global variant
260
+ className="my-form" // Optional: Root element class
261
+ ref={formRef} // Optional: Ref to the root <div> (React 19 ref-as-prop)
181
262
  />
182
263
  ```
183
264
 
184
265
  ### FormSchema
185
266
 
186
- ```tsx
267
+ ```ts
187
268
  interface FormSchema<T extends FieldValues = FieldValues> {
188
269
  sections: Section<T>[];
189
270
  }
271
+ ```
272
+
273
+ ### Section
190
274
 
275
+ ```ts
191
276
  interface Section<T> {
192
- id?: string; // Unique identifier
193
- title?: string; // Section title
194
- description?: string; // Section description
195
- icon?: ReactNode; // Section icon
196
- fields?: BaseField<T>[]; // Fields in this section
197
- cols?: number; // Grid columns (1-6)
198
- gap?: number; // Grid gap
199
- variant?: string; // Section variant
200
- className?: string; // Custom class
201
- collapsible?: boolean; // Make section collapsible
277
+ id?: string; // Unique identifier
278
+ title?: string; // Section title
279
+ description?: string; // Section description
280
+ icon?: ReactNode; // Section icon
281
+ fields?: BaseField<T>[]; // Fields in this section
282
+ cols?: number; // Grid columns (1-6)
283
+ gap?: number; // Grid gap
284
+ variant?: string; // Section variant
285
+ className?: string; // Custom class
286
+ collapsible?: boolean; // Make section collapsible
202
287
  defaultCollapsed?: boolean;
203
- condition?: (control) => boolean; // Conditional rendering
204
- render?: (props) => ReactNode; // Custom render function
288
+ nameSpace?: string; // Prefix for nested object fields (e.g. "address")
289
+
290
+ // Conditional rendering (function, DSL rule, or ConditionConfig)
291
+ condition?: Condition<T>;
292
+
293
+ // Custom render function (bypasses grid layout)
294
+ render?: (props: SectionRenderProps<T>) => ReactNode;
205
295
  }
206
296
  ```
207
297
 
208
298
  ### BaseField
209
299
 
210
- ```tsx
300
+ ```ts
211
301
  interface BaseField<T> {
212
- name: string; // Field name (required)
213
- type: FieldType; // Field type (required)
214
- label?: string; // Field label
215
- placeholder?: string; // Placeholder text
216
- helperText?: string; // Helper text below field
217
- disabled?: boolean; // Disable field
218
- required?: boolean; // Mark as required
219
- readOnly?: boolean; // Read-only field
220
- variant?: string; // Field variant
221
- fullWidth?: boolean; // Span full grid width
222
- className?: string; // Custom class
223
- defaultValue?: unknown; // Default value
224
-
302
+ name: string; // Field name (required)
303
+ type: FieldType; // Field type (required)
304
+ label?: string; // Field label
305
+ placeholder?: string; // Placeholder text
306
+ helperText?: string; // Helper text below field
307
+ disabled?: boolean; // Disable field
308
+ required?: boolean; // Mark as required
309
+ readOnly?: boolean; // Read-only field
310
+ variant?: string; // Field variant
311
+ fullWidth?: boolean; // Span full grid width
312
+ className?: string; // Custom class
313
+ defaultValue?: unknown; // Default value
314
+
225
315
  // Conditional rendering
226
- condition?: (formValues: T) => boolean;
227
-
316
+ condition?: Condition<T>;
317
+ watchNames?: string | string[]; // Optimize useWatch performance
318
+
319
+ // Dynamic options loading
320
+ loadOptions?: (
321
+ formValues: Partial<T>,
322
+ ) => Promise<FieldOption[]> | FieldOption[];
323
+ debounceMs?: number;
324
+
325
+ // For array/grouped types
326
+ itemFields?: BaseField<T>[];
327
+
228
328
  // For select/radio/checkbox
229
329
  options?: FieldOption[];
230
-
330
+
231
331
  // HTML input attributes
232
332
  min?: number | string;
233
333
  max?: number | string;
@@ -235,14 +335,17 @@ interface BaseField<T> {
235
335
  pattern?: string;
236
336
  minLength?: number;
237
337
  maxLength?: number;
238
- rows?: number; // For textarea
239
- multiple?: boolean; // For select/file
240
- accept?: string; // For file input
338
+ rows?: number;
339
+ multiple?: boolean;
340
+ accept?: string;
241
341
  autoComplete?: string;
242
342
  autoFocus?: boolean;
243
-
244
- // Custom props
245
- [key: string]: unknown;
343
+
344
+ // Custom render override
345
+ render?: (props: FieldComponentProps<T>) => ReactNode;
346
+
347
+ // Arbitrary extra props for custom components
348
+ customProps?: Record<string, unknown>;
246
349
  }
247
350
  ```
248
351
 
@@ -250,23 +353,66 @@ interface BaseField<T> {
250
353
 
251
354
  Props passed to your field components:
252
355
 
253
- ```tsx
254
- interface FieldComponentProps<T extends FieldValues = FieldValues> extends BaseField<T> {
255
- field: BaseField<T>; // Full field config
256
- control: Control<T>; // React Hook Form control
257
- disabled?: boolean; // Merged disabled state
258
- variant?: string; // Active variant
356
+ ```ts
357
+ interface FieldComponentProps<
358
+ T extends FieldValues = FieldValues,
359
+ > extends BaseField<T> {
360
+ field: BaseField<T>; // Full field config
361
+ control: Control<T>; // React Hook Form control
362
+ disabled?: boolean; // Merged disabled state
363
+ variant?: string; // Active variant
364
+ error?: FieldError; // Field error from react-hook-form
365
+ fieldState?: {
366
+ // Field state metadata
367
+ invalid: boolean;
368
+ isDirty: boolean;
369
+ isTouched: boolean;
370
+ isValidating: boolean;
371
+ error?: FieldError;
372
+ };
373
+ fieldId: string; // Generated ID for label-input association (e.g. "formkit-field-email")
259
374
  }
260
375
  ```
261
376
 
377
+ ### Condition Types
378
+
379
+ Conditions can be a function, a DSL rule, an array of rules (AND), or a `ConditionConfig` (AND/OR):
380
+
381
+ ```ts
382
+ // Function condition
383
+ condition: (values) => values.accountType === "business"
384
+
385
+ // Single DSL rule
386
+ condition: { watch: "country", operator: "===", value: "US" }
387
+
388
+ // Array of rules (AND - all must match)
389
+ condition: [
390
+ { watch: "country", operator: "===", value: "US" },
391
+ { watch: "age", operator: "truthy" },
392
+ ]
393
+
394
+ // ConditionConfig with OR logic
395
+ condition: {
396
+ rules: [
397
+ { watch: "country", operator: "===", value: "US" },
398
+ { watch: "country", operator: "===", value: "CA" },
399
+ ],
400
+ logic: "or",
401
+ }
402
+ ```
403
+
404
+ **Supported operators:** `===`, `!==`, `in`, `not-in`, `truthy`, `falsy`
405
+
406
+ **Nested paths:** DSL rules support dot-notation paths like `"address.city"` for nested form values.
407
+
262
408
  ### ComponentRegistry
263
409
 
264
- ```tsx
410
+ ```ts
265
411
  const components: ComponentRegistry = {
266
412
  // Simple mapping
267
413
  text: FormInput,
268
414
  select: FormSelect,
269
-
415
+
270
416
  // Variant-specific components
271
417
  compact: {
272
418
  text: CompactInput,
@@ -277,11 +423,11 @@ const components: ComponentRegistry = {
277
423
 
278
424
  ### LayoutRegistry
279
425
 
280
- ```tsx
426
+ ```ts
281
427
  const layouts: LayoutRegistry = {
282
428
  section: SectionLayout,
283
429
  grid: GridLayout,
284
-
430
+
285
431
  // Variant-specific layouts
286
432
  compact: {
287
433
  section: CompactSection,
@@ -289,11 +435,77 @@ const layouts: LayoutRegistry = {
289
435
  };
290
436
  ```
291
437
 
438
+ ### extractDefaultValues
439
+
440
+ Extracts default values from a schema. Server-safe (no hooks).
441
+
442
+ ```ts
443
+ import { extractDefaultValues } from "@classytic/formkit"; // or /server
444
+
445
+ const defaults = extractDefaultValues(formSchema);
446
+ // { firstName: "", lastName: "", email: "", password: "" }
447
+
448
+ // Use with react-hook-form
449
+ const form = useForm({ defaultValues: defaults });
450
+ ```
451
+
452
+ Respects `nameSpace` prefixes and group `itemFields` defaults.
453
+
454
+ ### buildValidationRules
455
+
456
+ Generates react-hook-form validation rules from a field's schema props. Server-safe (no hooks).
457
+
458
+ ```ts
459
+ import { buildValidationRules } from "@classytic/formkit"; // or /server
460
+
461
+ function FormInput({ field, control, error, fieldId }: FieldComponentProps) {
462
+ const rules = buildValidationRules(field);
463
+ return (
464
+ <Controller
465
+ name={field.name}
466
+ control={control}
467
+ rules={rules}
468
+ render={({ field: rhf }) => <input {...rhf} id={fieldId} />}
469
+ />
470
+ );
471
+ }
472
+ ```
473
+
474
+ Maps `required`, `min`, `max`, `minLength`, `maxLength`, and `pattern` from the field schema to RHF-compatible rules with auto-generated error messages.
475
+
476
+ ## Server Components
477
+
478
+ The `@classytic/formkit/server` entry point exports server-safe utilities with no React hooks or client-side code:
479
+
480
+ ```ts
481
+ import {
482
+ cn,
483
+ defineSchema,
484
+ defineField,
485
+ defineSection,
486
+ evaluateCondition,
487
+ extractWatchNames,
488
+ extractDefaultValues,
489
+ buildValidationRules,
490
+ } from "@classytic/formkit/server";
491
+
492
+ // Type-only imports also available
493
+ import type {
494
+ FormSchema,
495
+ BaseField,
496
+ Section,
497
+ ConditionRule,
498
+ ConditionConfig,
499
+ } from "@classytic/formkit/server";
500
+ ```
501
+
502
+ Use this entry point in React Server Components to define schemas, evaluate conditions, or use `cn` without pulling in client-side code.
503
+
292
504
  ## Advanced Features
293
505
 
294
- ### Conditional Fields
506
+ ### Conditional Fields (Function)
295
507
 
296
- ```tsx
508
+ ```ts
297
509
  {
298
510
  name: "companyName",
299
511
  type: "text",
@@ -302,6 +514,73 @@ const layouts: LayoutRegistry = {
302
514
  }
303
515
  ```
304
516
 
517
+ ### Conditional Fields (DSL Rules)
518
+
519
+ ```ts
520
+ {
521
+ name: "stateField",
522
+ type: "select",
523
+ label: "State",
524
+ condition: { watch: "country", operator: "===", value: "US" },
525
+ watchNames: ["country"], // Optimizes re-renders
526
+ }
527
+ ```
528
+
529
+ ### Conditional Sections
530
+
531
+ ```ts
532
+ {
533
+ title: "Business Details",
534
+ condition: (values) => values.accountType === "business",
535
+ fields: [
536
+ { name: "companyName", type: "text", label: "Company" },
537
+ { name: "taxId", type: "text", label: "Tax ID" },
538
+ ],
539
+ }
540
+ ```
541
+
542
+ ### OR Conditions
543
+
544
+ ```ts
545
+ {
546
+ name: "taxField",
547
+ type: "text",
548
+ condition: {
549
+ rules: [
550
+ { watch: "country", operator: "===", value: "US" },
551
+ { watch: "country", operator: "===", value: "CA" },
552
+ ],
553
+ logic: "or",
554
+ },
555
+ }
556
+ ```
557
+
558
+ ### Nested Path Conditions
559
+
560
+ DSL rules resolve dot-notation paths for nested form values:
561
+
562
+ ```ts
563
+ {
564
+ name: "zipCode",
565
+ type: "text",
566
+ condition: { watch: "address.country", operator: "===", value: "US" },
567
+ }
568
+ ```
569
+
570
+ ### Namespace Support
571
+
572
+ Prefix all field names in a section with a namespace for nested objects:
573
+
574
+ ```ts
575
+ {
576
+ nameSpace: "address",
577
+ fields: [
578
+ { name: "street", type: "text" }, // Becomes "address.street"
579
+ { name: "city", type: "text" }, // Becomes "address.city"
580
+ ],
581
+ }
582
+ ```
583
+
305
584
  ### Variants
306
585
 
307
586
  Apply different styles based on context:
@@ -315,25 +594,34 @@ const components = {
315
594
  },
316
595
  };
317
596
 
318
- // Use variant in schema
319
- const schema = {
320
- sections: [{
321
- variant: "compact", // All fields use compact variant
322
- fields: [...]
323
- }]
324
- };
597
+ // Use variant on the whole form
598
+ <FormGenerator schema={schema} variant="compact" />
599
+
600
+ // Or per-section
601
+ { variant: "compact", fields: [...] }
325
602
 
326
603
  // Or per-field
604
+ { name: "notes", type: "text", variant: "compact" }
605
+ ```
606
+
607
+ ### Dynamic Options Loading
608
+
609
+ ```ts
327
610
  {
328
- name: "notes",
329
- type: "text",
330
- variant: "compact",
611
+ name: "city",
612
+ type: "select",
613
+ watchNames: ["country"],
614
+ loadOptions: async (values) => {
615
+ const cities = await fetchCities(values.country);
616
+ return cities.map(c => ({ label: c.name, value: c.id }));
617
+ },
618
+ debounceMs: 300,
331
619
  }
332
620
  ```
333
621
 
334
622
  ### Custom Section Render
335
623
 
336
- ```tsx
624
+ ```ts
337
625
  {
338
626
  title: "Payment",
339
627
  render: ({ control, disabled }) => (
@@ -345,9 +633,46 @@ const schema = {
345
633
  }
346
634
  ```
347
635
 
348
- ### Grouped Select Options
636
+ ### Custom Field Render
637
+
638
+ ```ts
639
+ {
640
+ name: "avatar",
641
+ type: "file",
642
+ render: ({ field, control, error, fieldId }) => (
643
+ <AvatarUploader fieldId={fieldId} error={error} />
644
+ ),
645
+ }
646
+ ```
647
+
648
+ ### Custom Props
649
+
650
+ Pass arbitrary props to your field components via `customProps`:
651
+
652
+ ```ts
653
+ {
654
+ name: "bio",
655
+ type: "textarea",
656
+ label: "Biography",
657
+ customProps: {
658
+ maxCharacters: 500,
659
+ showCounter: true,
660
+ },
661
+ }
662
+ ```
663
+
664
+ Access in your component:
349
665
 
350
666
  ```tsx
667
+ function FormTextarea({ field, customProps, ...props }: FieldComponentProps) {
668
+ const maxChars = customProps?.maxCharacters as number;
669
+ // ...
670
+ }
671
+ ```
672
+
673
+ ### Grouped Select Options
674
+
675
+ ```ts
351
676
  {
352
677
  name: "country",
353
678
  type: "select",
@@ -370,31 +695,69 @@ const schema = {
370
695
  }
371
696
  ```
372
697
 
698
+ ### Schema Builder Utilities
699
+
700
+ Type-safe helpers for defining schemas outside of components:
701
+
702
+ ```ts
703
+ import {
704
+ defineSchema,
705
+ defineField,
706
+ defineSection,
707
+ } from "@classytic/formkit/server";
708
+
709
+ const emailField = defineField<MyFormData>({
710
+ name: "email",
711
+ type: "email",
712
+ label: "Email Address",
713
+ required: true,
714
+ });
715
+
716
+ const personalSection = defineSection<MyFormData>({
717
+ title: "Personal Info",
718
+ cols: 2,
719
+ fields: [emailField],
720
+ });
721
+
722
+ const schema = defineSchema<MyFormData>({
723
+ sections: [personalSection],
724
+ });
725
+ ```
726
+
373
727
  ## Type Exports
374
728
 
375
- ```tsx
729
+ ```ts
376
730
  import type {
377
731
  // Core
378
732
  FormSchema,
379
733
  FormGeneratorProps,
380
734
  BaseField,
381
735
  Section,
382
-
736
+
383
737
  // Components
384
738
  FieldComponentProps,
385
739
  FieldComponent,
386
740
  ComponentRegistry,
387
-
741
+
388
742
  // Layouts
389
743
  SectionLayoutProps,
390
744
  GridLayoutProps,
391
745
  LayoutComponent,
392
746
  LayoutRegistry,
393
-
747
+
394
748
  // Options
395
749
  FieldOption,
396
750
  FieldOptionGroup,
397
-
751
+
752
+ // Conditions
753
+ ConditionRule,
754
+ ConditionConfig,
755
+ Condition,
756
+
757
+ // Hook types
758
+ UseFormKitOptions,
759
+ UseFormKitReturn,
760
+
398
761
  // Utility types
399
762
  FieldType,
400
763
  LayoutType,
@@ -402,23 +765,12 @@ import type {
402
765
  DefineField,
403
766
  InferSchemaValues,
404
767
  SchemaFieldNames,
768
+ FormElement,
405
769
  } from "@classytic/formkit";
406
770
  ```
407
771
 
408
- ## Examples
409
-
410
- See the [`example/shadcn`](./example/shadcn) directory for complete working examples with:
411
-
412
- - Form components (Input, Select, Checkbox)
413
- - Full adapter configuration
414
- - Zod validation
415
- - Conditional fields
416
- - Multi-column layouts
417
- - TypeScript integration
418
-
419
772
  ## Browser Support
420
773
 
421
- - React 18.0+
422
774
  - React 19.0+
423
775
  - All modern browsers
424
776