@classytic/formkit 1.3.0 → 1.4.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/server.d.mts CHANGED
@@ -31,7 +31,7 @@ declare function cn(...inputs: ClassValue[]): string;
31
31
  * Field type identifier.
32
32
  * Can be built-in types or custom string identifiers.
33
33
  */
34
- type FieldType = "text" | "email" | "password" | "number" | "tel" | "url" | "textarea" | "select" | "checkbox" | "radio" | "switch" | "date" | "time" | "datetime" | "file" | "hidden" | "group" | "array" | "custom" | (string & {});
34
+ type FieldType = "text" | "email" | "password" | "number" | "tel" | "phone" | "url" | "textarea" | "slug" | "select" | "combobox" | "multiselect" | "dependentSelect" | "checkbox" | "radio" | "switch" | "date" | "time" | "datetime" | "rich-text" | "color" | "rating" | "tags" | "json" | "file" | "hidden" | "group" | "array" | "custom" | (string & {});
35
35
  /**
36
36
  * Layout type identifier.
37
37
  */
@@ -93,12 +93,55 @@ interface FieldOptionGroup<TValue = string> {
93
93
  /** Whether group is disabled */
94
94
  disabled?: boolean;
95
95
  }
96
+ /**
97
+ * Object form for length/range validation rules with a custom message.
98
+ */
99
+ interface ValidationRuleObject<TValue = number> {
100
+ value: TValue;
101
+ message: string;
102
+ }
103
+ /**
104
+ * Object form for pattern validation with a custom message.
105
+ */
106
+ interface PatternRuleObject {
107
+ /** Regex string (will be compiled with `new RegExp(regex)`) */
108
+ regex: string;
109
+ /** Custom error message */
110
+ message: string;
111
+ }
112
+ /**
113
+ * AI-friendly metadata attached to any field.
114
+ * Consumed by LLM coding assistants, documentation generators, and
115
+ * schema-introspection tools — never by the form renderer itself.
116
+ */
117
+ interface FieldMeta {
118
+ /** Human-readable description of what this field collects */
119
+ description?: string;
120
+ /** Representative example value (for documentation / AI context) */
121
+ example?: unknown;
122
+ /** Logical category for grouping fields in documentation or tooling */
123
+ category?: string;
124
+ /** Arbitrary tags for filtering, search, or feature-flagging */
125
+ tags?: string[];
126
+ /** Whether this field is considered PII */
127
+ pii?: boolean;
128
+ /** Arbitrary extension point — anything extra you want to carry */
129
+ [key: string]: unknown;
130
+ }
96
131
  /**
97
132
  * Base field configuration shared by all field types.
98
133
  * @template TFieldValues - Form field values type for type-safe field names
99
134
  */
100
135
  interface BaseField<TFieldValues extends FieldValues = FieldValues> {
101
- /** Field name (must be a valid path in form values) */
136
+ /**
137
+ * Field name — a path into TFieldValues (e.g. `"email"`, `"address.street"`).
138
+ *
139
+ * Accepts any string so that:
140
+ * - Namespaced sections can use relative names (`"street"` → prefixed to `"address.street"`)
141
+ * - Group/array `itemFields` can use relative names (`"email"` → prefixed at render time)
142
+ *
143
+ * Use `field.for<T>()` builder for call-site enforcement that names are valid paths.
144
+ */
102
145
  name: Path<TFieldValues> | (string & {});
103
146
  /** Field type identifier */
104
147
  type: FieldType;
@@ -129,18 +172,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
129
172
  defaultValue?: unknown;
130
173
  /** Options for select/radio/checkbox fields */
131
174
  options?: (FieldOption | FieldOptionGroup)[];
132
- /** Minimum value (for number/date fields) */
133
- min?: number | string;
134
- /** Maximum value (for number/date fields) */
135
- max?: number | string;
175
+ /**
176
+ * Minimum value (for number/date fields).
177
+ * Pass an object `{ value, message }` to provide a custom error message.
178
+ */
179
+ min?: number | string | ValidationRuleObject<number | string>;
180
+ /**
181
+ * Maximum value (for number/date fields).
182
+ * Pass an object `{ value, message }` to provide a custom error message.
183
+ */
184
+ max?: number | string | ValidationRuleObject<number | string>;
136
185
  /** Step value (for number fields) */
137
186
  step?: number;
138
- /** Pattern for validation (regex string) */
139
- pattern?: string;
140
- /** Minimum length */
141
- minLength?: number;
142
- /** Maximum length */
143
- maxLength?: number;
187
+ /**
188
+ * Pattern for validation.
189
+ * Pass a regex string or `{ regex, message }` for a custom error message.
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * // simple
194
+ * pattern: "^[a-z]+$"
195
+ * // with custom message
196
+ * pattern: { regex: "^[a-z]+$", message: "Only lowercase letters allowed" }
197
+ * ```
198
+ */
199
+ pattern?: string | PatternRuleObject;
200
+ /**
201
+ * Minimum length.
202
+ * Pass an object `{ value, message }` to provide a custom error message.
203
+ */
204
+ minLength?: number | ValidationRuleObject<number>;
205
+ /**
206
+ * Maximum length.
207
+ * Pass an object `{ value, message }` to provide a custom error message.
208
+ */
209
+ maxLength?: number | ValidationRuleObject<number>;
144
210
  /** Number of rows (for textarea) */
145
211
  rows?: number;
146
212
  /** Multiple selection (for select/file) */
@@ -164,10 +230,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
164
230
  */
165
231
  onLoadError?: (error: unknown) => void;
166
232
  /**
167
- * Fields for array or object types.
168
- * Useful for 'array' or 'group' field types that need a sub-schema.
233
+ * Sub-fields for `group` and `array` field types.
234
+ *
235
+ * These fields use **relative** names (`"street"`, `"email"`) — FormGenerator
236
+ * prefixes them with the parent field name at render time (`"address.street"`).
237
+ * They are intentionally untyped to TFieldValues for this reason.
169
238
  */
170
- itemFields?: BaseField<TFieldValues>[];
239
+ itemFields?: BaseField[];
171
240
  /**
172
241
  * Custom render function to override the component registry for this specific field.
173
242
  * Completely bypasses the globally registered FieldComponent for this type.
@@ -176,15 +245,22 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
176
245
  /**
177
246
  * Cross-field validation function.
178
247
  * Receives the field value and all form values for cross-field checks.
179
- * Return `true` for valid, or a string error message for invalid.
248
+ * Return `true` for valid, a string error message for invalid, or a Promise of either
249
+ * for async validation (e.g., checking server-side uniqueness).
180
250
  *
181
251
  * @example
182
252
  * ```ts
183
253
  * validate: (value, formValues) =>
184
254
  * value > formValues.minPrice || "Must be greater than min price"
255
+ *
256
+ * // Async example
257
+ * validate: async (value) => {
258
+ * const taken = await checkUsernameAvailability(value as string);
259
+ * return taken ? "Username already taken" : true;
260
+ * }
185
261
  * ```
186
262
  */
187
- validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true;
263
+ validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true | Promise<string | true>;
188
264
  /**
189
265
  * Dependencies for optimizing conditionally rendered fields.
190
266
  * Allows specifying specific field names to watch, preventing full form re-renders.
@@ -192,6 +268,32 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
192
268
  watchNames?: Path<TFieldValues> | Path<TFieldValues>[];
193
269
  /** Additional field-specific props for custom components */
194
270
  customProps?: Record<string, unknown>;
271
+ /**
272
+ * AI / agent-friendly metadata for this field.
273
+ *
274
+ * Provides hints that help LLM coding assistants generate correct field
275
+ * configs without needing to inspect the schema at runtime.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * field.text("companyName", "Company", {
280
+ * meta: {
281
+ * description: "Legal entity name of the organization",
282
+ * example: "Acme Corp",
283
+ * category: "identity",
284
+ * tags: ["crm", "required-for-billing"],
285
+ * }
286
+ * })
287
+ * ```
288
+ */
289
+ meta?: FieldMeta;
290
+ /**
291
+ * Escape hatch for adapter-specific or custom props.
292
+ * Allows passing arbitrary props directly on the field object so they
293
+ * flow through the adapter spread (`{...field}`) without needing `customProps`.
294
+ * Intentionally broad to support diverse UI component libraries.
295
+ */
296
+ [key: string]: unknown;
195
297
  }
196
298
  /**
197
299
  * Props passed to field components.
@@ -237,13 +339,68 @@ interface FieldComponentProps<TFieldValues extends FieldValues = FieldValues> ex
237
339
  invalid: boolean;
238
340
  isDirty: boolean;
239
341
  isTouched: boolean;
240
- isValidating: boolean;
342
+ isValidating: boolean; /** True after the enclosing form's submit handler has been called at least once. */
343
+ isSubmitted: boolean;
241
344
  error?: FieldError;
242
345
  };
243
- /** Generated field ID for label-input association (e.g. `formkit-field-email`) */
346
+ /**
347
+ * Generated field ID for label-input association (e.g. `formkit-field-email`).
348
+ * Use as `id` on the input element and `htmlFor` on the `<label>`.
349
+ */
244
350
  fieldId: string;
351
+ /**
352
+ * Generated error container ID for ARIA association (e.g. `formkit-field-email-error`).
353
+ * Use as `id` on the error message element and as the value for `aria-errormessage`
354
+ * (preferred) or `aria-describedby` (broader support) on the input.
355
+ *
356
+ * @example
357
+ * ```tsx
358
+ * <input
359
+ * id={fieldId}
360
+ * aria-invalid={shouldShowError || undefined}
361
+ * aria-errormessage={shouldShowError ? errorId : undefined}
362
+ * />
363
+ * <p id={errorId} role="alert" aria-live="polite">
364
+ * {shouldShowError ? error?.message : null}
365
+ * </p>
366
+ * ```
367
+ */
368
+ errorId: string;
369
+ /**
370
+ * Whether to display the field error right now.
371
+ *
372
+ * Aligns with the CSS `:user-invalid` timing model: `true` only after the
373
+ * user has interacted with the field (blur) **or** the form has been
374
+ * submitted. This prevents premature "required" errors on untouched fields.
375
+ *
376
+ * Use this — not `!!error` — to drive `aria-invalid`, error message
377
+ * visibility, and destructive ring/border styles.
378
+ *
379
+ * @example
380
+ * ```tsx
381
+ * <input aria-invalid={shouldShowError || undefined} />
382
+ * {shouldShowError && <p id={errorId} role="alert">{error?.message}</p>}
383
+ * ```
384
+ */
385
+ shouldShowError: boolean;
245
386
  /** Whether dynamic options are currently loading */
246
387
  isLoading?: boolean;
388
+ /**
389
+ * Pre-computed react-hook-form validation rules for this field.
390
+ * Equivalent to calling `buildValidationRules(field)` — provided here so
391
+ * adapter components can pass `rules={rules}` directly to `<Controller>`
392
+ * without importing or calling `buildValidationRules` themselves.
393
+ *
394
+ * @example
395
+ * ```tsx
396
+ * function TextInput({ field, control, rules }: FieldComponentProps) {
397
+ * return (
398
+ * <Controller name={field.name} control={control} rules={rules} render={...} />
399
+ * );
400
+ * }
401
+ * ```
402
+ */
403
+ rules: ValidationRules;
247
404
  }
248
405
  /**
249
406
  * Validation rules compatible with react-hook-form's RegisterOptions.
@@ -289,6 +446,17 @@ interface Section<TFieldValues extends FieldValues = FieldValues> {
289
446
  collapsible?: boolean;
290
447
  /** Default collapsed state */
291
448
  defaultCollapsed?: boolean;
449
+ /**
450
+ * Defer rendering of this section until it scrolls near the viewport.
451
+ * Applies `content-visibility: auto` + `contain-intrinsic-size` to the
452
+ * section container, skipping layout/paint work while off-screen.
453
+ *
454
+ * **Only use for sections that are below the initial fold.** Applying this
455
+ * to above-fold sections has no benefit and slightly increases overhead.
456
+ *
457
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility
458
+ */
459
+ deferRender?: boolean;
292
460
  }
293
461
  /**
294
462
  * Props passed to section render function.
@@ -316,6 +484,12 @@ interface SectionLayoutProps {
316
484
  collapsible?: boolean;
317
485
  /** Default collapsed state */
318
486
  defaultCollapsed?: boolean;
487
+ /**
488
+ * When true the section is below the initial fold and should defer
489
+ * browser layout/paint work until it nears the viewport.
490
+ * Layout components may apply `content-visibility: auto` here.
491
+ */
492
+ deferRender?: boolean;
319
493
  /** Children content */
320
494
  children: ReactNode;
321
495
  }
@@ -350,6 +524,12 @@ type LayoutComponentProps = SectionLayoutProps | GridLayoutProps | DefaultLayout
350
524
  * @template TFieldValues - Form field values type for type-safe schemas
351
525
  */
352
526
  interface FormSchema<TFieldValues extends FieldValues = FieldValues> {
527
+ /** Optional schema identifier — useful for serialization, analytics, and AI context */
528
+ id?: string;
529
+ /** Human-readable form title (AI / documentation use) */
530
+ title?: string;
531
+ /** Human-readable description of the form's purpose */
532
+ description?: string;
353
533
  /** Form sections */
354
534
  sections: Section<TFieldValues>[];
355
535
  }
@@ -402,6 +582,7 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
402
582
  /**
403
583
  * Extracts default values from a form schema.
404
584
  * Walks all sections and fields, respecting nameSpace prefixes and group nesting.
585
+ * Array fields default to `[]` when no explicit `defaultValue` is provided.
405
586
  *
406
587
  * @example
407
588
  * ```ts
@@ -415,6 +596,9 @@ declare function extractDefaultValues<TFieldValues extends FieldValues = FieldVa
415
596
  * from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
416
597
  * `maxLength`, `pattern`, and `validate` to RHF rules.
417
598
  *
599
+ * Supports both shorthand scalars and `{ value, message }` objects for all
600
+ * numeric/length rules, and `{ regex, message }` for pattern.
601
+ *
418
602
  * @example
419
603
  * ```tsx
420
604
  * import { buildValidationRules } from '@classytic/formkit';
@@ -426,175 +610,147 @@ declare function extractDefaultValues<TFieldValues extends FieldValues = FieldVa
426
610
  * ```
427
611
  */
428
612
  declare function buildValidationRules<TFieldValues extends FieldValues = FieldValues>(field: BaseField<TFieldValues>): ValidationRules;
613
+ /** Returns true for fields that carry an `options` array (select, radio, etc.) */
614
+ declare function isChoiceField(field: BaseField): boolean;
615
+ /** Returns true for free-text input fields */
616
+ declare function isTextField(field: BaseField): boolean;
617
+ /** Returns true for numeric input fields */
618
+ declare function isNumericField(field: BaseField): boolean;
619
+ /** Returns true for date / time fields */
620
+ declare function isDateField(field: BaseField): boolean;
621
+ /** Returns true for structural fields that contain sub-fields (`itemFields`) */
622
+ declare function isContainerField(field: BaseField): boolean;
623
+ /** Returns true for array fields that render a repeatable list */
624
+ declare function isArrayField(field: BaseField): boolean;
625
+ /** Returns true for fields that load options asynchronously */
626
+ declare function isDynamicField(field: BaseField): boolean;
627
+ /** Returns true for fields with conditional rendering */
628
+ declare function isConditionalField(field: BaseField): boolean;
629
+ /**
630
+ * Merge two or more schemas into one, concatenating their sections.
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * const full = mergeSchemas(personalSchema, addressSchema, billingSchema);
635
+ * ```
636
+ */
637
+ declare function mergeSchemas<TFieldValues extends FieldValues = FieldValues>(...schemas: FormSchema<TFieldValues>[]): FormSchema<TFieldValues>;
638
+ /**
639
+ * Add fields to a section identified by `sectionId`.
640
+ * Returns a new schema — the original is not mutated.
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * const extended = extendSection(schema, "personal", [
645
+ * field.text("middleName", "Middle Name"),
646
+ * ]);
647
+ * ```
648
+ */
649
+ declare function extendSection<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, sectionId: string, fields: BaseField<TFieldValues>[], position?: "start" | "end"): FormSchema<TFieldValues>;
650
+ /**
651
+ * Create a new schema that includes only the named fields.
652
+ *
653
+ * @example
654
+ * ```ts
655
+ * const slim = pickFields(schema, ["email", "password"]);
656
+ * ```
657
+ */
658
+ declare function pickFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
659
+ /**
660
+ * Create a new schema that excludes the named fields.
661
+ *
662
+ * @example
663
+ * ```ts
664
+ * const withoutInternal = omitFields(schema, ["__id", "__createdAt"]);
665
+ * ```
666
+ */
667
+ declare function omitFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
668
+ /**
669
+ * Collect every field from every section into a flat array.
670
+ * Useful for validation, documentation, and AI schema introspection.
671
+ *
672
+ * @example
673
+ * ```ts
674
+ * const allFields = flattenSchema(schema);
675
+ * const required = allFields.filter(f => f.required);
676
+ * ```
677
+ */
678
+ declare function flattenSchema<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): BaseField<TFieldValues>[];
429
679
  //#endregion
430
680
  //#region src/builders.d.ts
431
681
  /**
432
- * Additional field props for builder helpers.
682
+ * Additional field props accepted by builder helpers.
433
683
  * Accepts all BaseField properties except `name`, `type`, and `label`
434
- * which are set by the builder method.
684
+ * which are provided by the builder method directly.
435
685
  */
436
- type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label"> & {
437
- /** Grid column class (e.g., "col-span-2") */gridColumn?: string; /** Icon for the left side of input */
438
- iconLeft?: ReactNode; /** Icon for the right side of input */
439
- iconRight?: ReactNode; /** Additional custom props */
440
- [key: string]: unknown;
441
- };
686
+ type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label">;
442
687
  /**
443
688
  * Section configuration props.
444
689
  */
445
690
  interface SectionProps<TFieldValues extends FieldValues = FieldValues> extends Omit<Section<TFieldValues>, "id" | "title" | "fields" | "cols"> {
446
691
  cols?: number;
447
692
  }
448
- /**
449
- * Render function for custom field types.
450
- */
451
- type CustomRenderFn = (props: {
452
- control: Control<FieldValues>;
453
- disabled?: boolean;
454
- error?: FieldError;
455
- }) => ReactNode;
456
693
  /**
457
694
  * Type-safe field builder helpers for schema-driven forms.
458
695
  *
459
- * Provides shorthand methods for common field types with sensible defaults,
460
- * reducing boilerplate while maintaining full type safety.
696
+ * All methods are generic over TFieldValues, defaulting to FieldValues (any string)
697
+ * when no type argument is provided. Specify the generic to enforce that field
698
+ * names are valid paths in your form values type.
699
+ *
700
+ * For fully-typed schemas where every field name is checked, prefer
701
+ * `field.for<MyForm>()` which fixes the generic once for the whole schema:
461
702
  *
462
703
  * @example
463
704
  * ```ts
464
- * import { field, section } from '@classytic/formkit';
705
+ * // Untyped any string accepted (backwards compatible)
706
+ * field.text("email", "Email")
465
707
  *
466
- * const schema = {
467
- * sections: [
468
- * section("personal", "Personal Info", [
469
- * field.text("firstName", "First Name", { required: true }),
470
- * field.email("email", "Email"),
471
- * field.select("role", "Role", [
472
- * { label: "Admin", value: "admin" },
473
- * { label: "User", value: "user" },
474
- * ]),
475
- * ], { cols: 2 }),
476
- * ],
477
- * };
708
+ * // Per-call generic — name is checked against MyForm
709
+ * field.text<MyForm>("email", "Email")
710
+ *
711
+ * // Typed factory name checked on every call without repeating the generic
712
+ * const f = field.for<MyForm>()
713
+ * f.text("email", "Email") //
714
+ * f.text("typo", "Email") // ✗ TypeScript error
478
715
  * ```
479
716
  */
480
717
  declare const field: {
481
- /**
482
- * Text input field.
483
- */
484
- text: (name: string, label: string, props?: FieldProps) => BaseField;
485
- /**
486
- * Email input field with default placeholder.
487
- */
488
- email: (name: string, label: string, props?: FieldProps) => BaseField;
489
- /**
490
- * URL input field with default placeholder.
491
- */
492
- url: (name: string, label: string, props?: FieldProps) => BaseField;
493
- /**
494
- * Phone/tel input field with default placeholder.
495
- */
496
- tel: (name: string, label: string, props?: FieldProps) => BaseField;
497
- /**
498
- * Password input field.
499
- */
500
- password: (name: string, label: string, props?: FieldProps) => BaseField;
501
- /**
502
- * Number input field with min: 0 default.
503
- */
504
- number: (name: string, label: string, props?: FieldProps) => BaseField;
505
- /**
506
- * Textarea field with default 3 rows.
507
- */
508
- textarea: (name: string, label: string, props?: FieldProps) => BaseField;
509
- /**
510
- * Select dropdown field.
511
- */
512
- select: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
513
- /**
514
- * Searchable combobox field.
515
- */
516
- combobox: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
517
- /**
518
- * Multi-select field (tag choice).
519
- */
520
- multiselect: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
521
- /**
522
- * Tag choice field for selecting options as tags/chips.
523
- */
524
- tagChoice: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
525
- /**
526
- * Dependent select field that reacts to parent field changes.
527
- */
528
- dependentSelect: (name: string, label: string, props?: FieldProps) => BaseField;
529
- /**
530
- * Switch/toggle field.
531
- */
532
- switch: (name: string, label: string, props?: FieldProps) => BaseField;
533
- /**
534
- * Boolean field (alias for switch).
535
- */
536
- boolean: (name: string, label: string, props?: FieldProps) => BaseField;
537
- /**
538
- * Checkbox field.
539
- */
540
- checkbox: (name: string, label: string, props?: FieldProps) => BaseField;
541
- /**
542
- * Radio button group field.
543
- */
544
- radio: (name: string, label: string, options: FieldOption[], props?: FieldProps) => BaseField;
545
- /**
546
- * Date picker field.
547
- */
548
- date: (name: string, label: string, props?: FieldProps) => BaseField;
549
- /**
550
- * Tag input field with default placeholder.
551
- */
552
- tags: (name: string, label: string, props?: FieldProps) => BaseField;
553
- /**
554
- * Slug field with auto-generation from source value.
555
- */
556
- slug: (name: string, label: string, props?: FieldProps) => BaseField;
557
- /**
558
- * File upload field.
559
- */
560
- file: (name: string, label: string, props?: FieldProps) => BaseField;
561
- /**
562
- * OTP/PIN input field.
563
- */
564
- otp: (name: string, label: string, props?: FieldProps) => BaseField;
565
- /**
566
- * Async searchable combobox with server-side search.
567
- */
568
- asyncCombobox: (name: string, label: string, props?: FieldProps) => BaseField;
569
- /**
570
- * Async searchable multi-select with server-side search.
571
- */
572
- asyncMultiselect: (name: string, label: string, props?: FieldProps) => BaseField;
573
- /**
574
- * Date and optional time picker field.
575
- */
576
- dateTime: (name: string, label: string, props?: FieldProps) => BaseField;
577
- /**
578
- * Hidden field (no UI).
579
- */
580
- hidden: (name: string, props?: FieldProps) => BaseField;
718
+ /** Text input field. */text: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Email input field with default placeholder. */
719
+ email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
720
+ url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
721
+ tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
722
+ password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Number input field with min: 0 default (overrideable via props). */
723
+ number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
724
+ textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
725
+ select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
726
+ combobox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Multi-select field. */
727
+ multiselect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Dependent select field that reacts to parent field changes. */
728
+ dependentSelect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Switch/toggle field. */
729
+ switch: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Boolean field (alias for switch). */
730
+ boolean: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Checkbox field. */
731
+ checkbox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Radio button group field. */
732
+ radio: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>; /** Date picker field. */
733
+ date: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Tag input field. */
734
+ tags: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Slug field. */
735
+ slug: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** File upload field. */
736
+ file: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Hidden field (no UI). */
737
+ hidden: <T extends FieldValues = FieldValues>(name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
581
738
  /**
582
739
  * Group field for nested objects.
583
- * Renders itemFields as a sub-grid within the form.
740
+ * Renders itemFields as a sub-grid. Child names are relative (e.g. "street"),
741
+ * FormGenerator prefixes them with the group name at render time.
584
742
  *
585
743
  * @example
586
744
  * ```ts
587
745
  * field.group("address", "Address", [
588
746
  * field.text("street", "Street"),
589
747
  * field.text("city", "City"),
590
- * field.text("zip", "ZIP Code"),
591
- * ], { cols: 3 })
748
+ * ], { cols: 2 })
592
749
  * ```
593
750
  */
594
- group: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
751
+ group: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
595
752
  /**
596
- * Array/repeatable field.
597
- * Renders a dynamic list of sub-forms using react-hook-form's useFieldArray.
753
+ * Array/repeatable field backed by react-hook-form's useFieldArray.
598
754
  *
599
755
  * @example
600
756
  * ```ts
@@ -604,41 +760,108 @@ declare const field: {
604
760
  * ])
605
761
  * ```
606
762
  */
607
- array: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
763
+ array: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
608
764
  /**
609
765
  * Custom field with a render function.
610
- * Bypasses the component registry entirely.
766
+ * Bypasses the component registry — full control over rendering.
767
+ *
768
+ * The render callback receives the complete `FieldComponentProps` including
769
+ * `fieldId`, `errorId`, `shouldShowError`, `error`, `rules`, and `control`.
770
+ *
771
+ * Use `shouldShowError` (not `!!error`) to drive `aria-invalid` and error
772
+ * visibility so timing mirrors the CSS `:user-invalid` pseudo-class.
611
773
  *
612
774
  * @example
613
- * ```ts
614
- * field.custom("skills", "Skills", ({ control, disabled }) => (
615
- * <SkillSelector control={control} disabled={disabled} />
775
+ * ```tsx
776
+ * field.custom("skills", "Skills", ({ control, shouldShowError, errorId, error, fieldId }) => (
777
+ * <div>
778
+ * <SkillSelector
779
+ * id={fieldId}
780
+ * control={control}
781
+ * aria-invalid={shouldShowError || undefined}
782
+ * aria-errormessage={shouldShowError ? errorId : undefined}
783
+ * />
784
+ * {shouldShowError && (
785
+ * <p id={errorId} role="alert" className="text-sm text-destructive">
786
+ * {error?.message}
787
+ * </p>
788
+ * )}
789
+ * </div>
616
790
  * ))
617
791
  * ```
618
792
  */
619
- custom: (name: string, label: string, render: CustomRenderFn, props?: FieldProps) => BaseField;
793
+ custom: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, props?: FieldProps<T>) => BaseField<T>;
794
+ /**
795
+ * Returns a typed field builder with `TFieldValues` fixed.
796
+ * Every field name is validated against `Path<TFieldValues>` at the call site —
797
+ * no need to repeat the generic on each individual builder call.
798
+ *
799
+ * @example
800
+ * ```ts
801
+ * interface ContactForm {
802
+ * firstName: string;
803
+ * email: string;
804
+ * address: { street: string; city: string };
805
+ * }
806
+ *
807
+ * const f = field.for<ContactForm>()
808
+ *
809
+ * const schema = defineSchema<ContactForm>({
810
+ * sections: [{
811
+ * fields: [
812
+ * f.text("firstName", "First Name"), // ✓
813
+ * f.email("email", "Email"), // ✓
814
+ * f.text("typo", "Label"), // ✗ TypeScript error
815
+ * ],
816
+ * }],
817
+ * })
818
+ * ```
819
+ */
820
+ for: <T extends FieldValues>() => {
821
+ text: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
822
+ email: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
823
+ url: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
824
+ tel: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
825
+ password: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
826
+ number: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
827
+ textarea: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
828
+ select: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
829
+ combobox: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
830
+ multiselect: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
831
+ dependentSelect: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
832
+ switch: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
833
+ boolean: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
834
+ checkbox: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
835
+ radio: (name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>;
836
+ date: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
837
+ tags: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
838
+ slug: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
839
+ file: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
840
+ hidden: (name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
841
+ group: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
842
+ array: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
843
+ custom: (name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, builderProps?: FieldProps<T>) => BaseField<T>;
844
+ };
620
845
  };
621
846
  /**
622
847
  * Create a section definition with sensible defaults.
623
848
  *
624
- * @param id - Unique section identifier
625
- * @param title - Section title
626
- * @param fields - Array of field definitions
627
- * @param props - Additional section configuration
628
- *
629
849
  * @example
630
850
  * ```ts
631
851
  * section("personal", "Personal Info", [
632
852
  * field.text("name", "Name", { required: true }),
633
853
  * field.email("email", "Email"),
634
- * ], { cols: 2, variant: "card" })
854
+ * ], { cols: 2 })
635
855
  * ```
636
856
  */
637
857
  declare function section<TFieldValues extends FieldValues = FieldValues>(id: string, title: string, fields: BaseField<TFieldValues>[], props?: SectionProps<TFieldValues>): Section<TFieldValues>;
638
858
  /**
639
859
  * Create a section without a title (transparent section).
640
860
  * Useful for grouping fields without visual separation.
861
+ *
862
+ * Accepts `BaseField[]` (no generic) so mixed-type field arrays don't trigger
863
+ * conflicting type inference across different field name generics.
641
864
  */
642
- declare function sectionUntitled<TFieldValues extends FieldValues = FieldValues>(fields: BaseField<TFieldValues>[], props?: Omit<SectionProps<TFieldValues>, "variant">): Section<TFieldValues>;
865
+ declare function sectionUntitled(fields: BaseField[], props?: Omit<SectionProps, "variant">): Section;
643
866
  //#endregion
644
- export { type BaseField, type ClassValue, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldOption, type FieldOptionGroup, type FieldType, type FormSchema, type GridLayoutProps, type InferSchemaValues, type LayoutComponentProps, type LayoutType, type SchemaFieldNames, type Section, type SectionLayoutProps, type SectionRenderProps, type Variant, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extractDefaultValues, extractWatchNames, field, section, sectionUntitled };
867
+ export { type BaseField, type ClassValue, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldMeta, type FieldOption, type FieldOptionGroup, type FieldType, type FormSchema, type GridLayoutProps, type InferSchemaValues, type LayoutComponentProps, type LayoutType, type PatternRuleObject, type SchemaFieldNames, type Section, type SectionLayoutProps, type SectionRenderProps, type ValidationRuleObject, type Variant, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeSchemas, omitFields, pickFields, section, sectionUntitled };