@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/index.d.mts CHANGED
@@ -8,7 +8,7 @@ import { ClassValue } from "clsx";
8
8
  * Field type identifier.
9
9
  * Can be built-in types or custom string identifiers.
10
10
  */
11
- type FieldType = "text" | "email" | "password" | "number" | "tel" | "url" | "textarea" | "select" | "checkbox" | "radio" | "switch" | "date" | "time" | "datetime" | "file" | "hidden" | "group" | "array" | "custom" | (string & {});
11
+ 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 & {});
12
12
  /**
13
13
  * Layout type identifier.
14
14
  */
@@ -70,12 +70,55 @@ interface FieldOptionGroup<TValue = string> {
70
70
  /** Whether group is disabled */
71
71
  disabled?: boolean;
72
72
  }
73
+ /**
74
+ * Object form for length/range validation rules with a custom message.
75
+ */
76
+ interface ValidationRuleObject<TValue = number> {
77
+ value: TValue;
78
+ message: string;
79
+ }
80
+ /**
81
+ * Object form for pattern validation with a custom message.
82
+ */
83
+ interface PatternRuleObject {
84
+ /** Regex string (will be compiled with `new RegExp(regex)`) */
85
+ regex: string;
86
+ /** Custom error message */
87
+ message: string;
88
+ }
89
+ /**
90
+ * AI-friendly metadata attached to any field.
91
+ * Consumed by LLM coding assistants, documentation generators, and
92
+ * schema-introspection tools — never by the form renderer itself.
93
+ */
94
+ interface FieldMeta {
95
+ /** Human-readable description of what this field collects */
96
+ description?: string;
97
+ /** Representative example value (for documentation / AI context) */
98
+ example?: unknown;
99
+ /** Logical category for grouping fields in documentation or tooling */
100
+ category?: string;
101
+ /** Arbitrary tags for filtering, search, or feature-flagging */
102
+ tags?: string[];
103
+ /** Whether this field is considered PII */
104
+ pii?: boolean;
105
+ /** Arbitrary extension point — anything extra you want to carry */
106
+ [key: string]: unknown;
107
+ }
73
108
  /**
74
109
  * Base field configuration shared by all field types.
75
110
  * @template TFieldValues - Form field values type for type-safe field names
76
111
  */
77
112
  interface BaseField<TFieldValues extends FieldValues = FieldValues> {
78
- /** Field name (must be a valid path in form values) */
113
+ /**
114
+ * Field name — a path into TFieldValues (e.g. `"email"`, `"address.street"`).
115
+ *
116
+ * Accepts any string so that:
117
+ * - Namespaced sections can use relative names (`"street"` → prefixed to `"address.street"`)
118
+ * - Group/array `itemFields` can use relative names (`"email"` → prefixed at render time)
119
+ *
120
+ * Use `field.for<T>()` builder for call-site enforcement that names are valid paths.
121
+ */
79
122
  name: Path<TFieldValues> | (string & {});
80
123
  /** Field type identifier */
81
124
  type: FieldType;
@@ -106,18 +149,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
106
149
  defaultValue?: unknown;
107
150
  /** Options for select/radio/checkbox fields */
108
151
  options?: (FieldOption | FieldOptionGroup)[];
109
- /** Minimum value (for number/date fields) */
110
- min?: number | string;
111
- /** Maximum value (for number/date fields) */
112
- max?: number | string;
152
+ /**
153
+ * Minimum value (for number/date fields).
154
+ * Pass an object `{ value, message }` to provide a custom error message.
155
+ */
156
+ min?: number | string | ValidationRuleObject<number | string>;
157
+ /**
158
+ * Maximum value (for number/date fields).
159
+ * Pass an object `{ value, message }` to provide a custom error message.
160
+ */
161
+ max?: number | string | ValidationRuleObject<number | string>;
113
162
  /** Step value (for number fields) */
114
163
  step?: number;
115
- /** Pattern for validation (regex string) */
116
- pattern?: string;
117
- /** Minimum length */
118
- minLength?: number;
119
- /** Maximum length */
120
- maxLength?: number;
164
+ /**
165
+ * Pattern for validation.
166
+ * Pass a regex string or `{ regex, message }` for a custom error message.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // simple
171
+ * pattern: "^[a-z]+$"
172
+ * // with custom message
173
+ * pattern: { regex: "^[a-z]+$", message: "Only lowercase letters allowed" }
174
+ * ```
175
+ */
176
+ pattern?: string | PatternRuleObject;
177
+ /**
178
+ * Minimum length.
179
+ * Pass an object `{ value, message }` to provide a custom error message.
180
+ */
181
+ minLength?: number | ValidationRuleObject<number>;
182
+ /**
183
+ * Maximum length.
184
+ * Pass an object `{ value, message }` to provide a custom error message.
185
+ */
186
+ maxLength?: number | ValidationRuleObject<number>;
121
187
  /** Number of rows (for textarea) */
122
188
  rows?: number;
123
189
  /** Multiple selection (for select/file) */
@@ -141,10 +207,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
141
207
  */
142
208
  onLoadError?: (error: unknown) => void;
143
209
  /**
144
- * Fields for array or object types.
145
- * Useful for 'array' or 'group' field types that need a sub-schema.
210
+ * Sub-fields for `group` and `array` field types.
211
+ *
212
+ * These fields use **relative** names (`"street"`, `"email"`) — FormGenerator
213
+ * prefixes them with the parent field name at render time (`"address.street"`).
214
+ * They are intentionally untyped to TFieldValues for this reason.
146
215
  */
147
- itemFields?: BaseField<TFieldValues>[];
216
+ itemFields?: BaseField[];
148
217
  /**
149
218
  * Custom render function to override the component registry for this specific field.
150
219
  * Completely bypasses the globally registered FieldComponent for this type.
@@ -153,15 +222,22 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
153
222
  /**
154
223
  * Cross-field validation function.
155
224
  * Receives the field value and all form values for cross-field checks.
156
- * Return `true` for valid, or a string error message for invalid.
225
+ * Return `true` for valid, a string error message for invalid, or a Promise of either
226
+ * for async validation (e.g., checking server-side uniqueness).
157
227
  *
158
228
  * @example
159
229
  * ```ts
160
230
  * validate: (value, formValues) =>
161
231
  * value > formValues.minPrice || "Must be greater than min price"
232
+ *
233
+ * // Async example
234
+ * validate: async (value) => {
235
+ * const taken = await checkUsernameAvailability(value as string);
236
+ * return taken ? "Username already taken" : true;
237
+ * }
162
238
  * ```
163
239
  */
164
- validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true;
240
+ validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true | Promise<string | true>;
165
241
  /**
166
242
  * Dependencies for optimizing conditionally rendered fields.
167
243
  * Allows specifying specific field names to watch, preventing full form re-renders.
@@ -169,6 +245,32 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
169
245
  watchNames?: Path<TFieldValues> | Path<TFieldValues>[];
170
246
  /** Additional field-specific props for custom components */
171
247
  customProps?: Record<string, unknown>;
248
+ /**
249
+ * AI / agent-friendly metadata for this field.
250
+ *
251
+ * Provides hints that help LLM coding assistants generate correct field
252
+ * configs without needing to inspect the schema at runtime.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * field.text("companyName", "Company", {
257
+ * meta: {
258
+ * description: "Legal entity name of the organization",
259
+ * example: "Acme Corp",
260
+ * category: "identity",
261
+ * tags: ["crm", "required-for-billing"],
262
+ * }
263
+ * })
264
+ * ```
265
+ */
266
+ meta?: FieldMeta;
267
+ /**
268
+ * Escape hatch for adapter-specific or custom props.
269
+ * Allows passing arbitrary props directly on the field object so they
270
+ * flow through the adapter spread (`{...field}`) without needing `customProps`.
271
+ * Intentionally broad to support diverse UI component libraries.
272
+ */
273
+ [key: string]: unknown;
172
274
  }
173
275
  /**
174
276
  * Props passed to field components.
@@ -214,13 +316,68 @@ interface FieldComponentProps<TFieldValues extends FieldValues = FieldValues> ex
214
316
  invalid: boolean;
215
317
  isDirty: boolean;
216
318
  isTouched: boolean;
217
- isValidating: boolean;
319
+ isValidating: boolean; /** True after the enclosing form's submit handler has been called at least once. */
320
+ isSubmitted: boolean;
218
321
  error?: FieldError;
219
322
  };
220
- /** Generated field ID for label-input association (e.g. `formkit-field-email`) */
323
+ /**
324
+ * Generated field ID for label-input association (e.g. `formkit-field-email`).
325
+ * Use as `id` on the input element and `htmlFor` on the `<label>`.
326
+ */
221
327
  fieldId: string;
328
+ /**
329
+ * Generated error container ID for ARIA association (e.g. `formkit-field-email-error`).
330
+ * Use as `id` on the error message element and as the value for `aria-errormessage`
331
+ * (preferred) or `aria-describedby` (broader support) on the input.
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * <input
336
+ * id={fieldId}
337
+ * aria-invalid={shouldShowError || undefined}
338
+ * aria-errormessage={shouldShowError ? errorId : undefined}
339
+ * />
340
+ * <p id={errorId} role="alert" aria-live="polite">
341
+ * {shouldShowError ? error?.message : null}
342
+ * </p>
343
+ * ```
344
+ */
345
+ errorId: string;
346
+ /**
347
+ * Whether to display the field error right now.
348
+ *
349
+ * Aligns with the CSS `:user-invalid` timing model: `true` only after the
350
+ * user has interacted with the field (blur) **or** the form has been
351
+ * submitted. This prevents premature "required" errors on untouched fields.
352
+ *
353
+ * Use this — not `!!error` — to drive `aria-invalid`, error message
354
+ * visibility, and destructive ring/border styles.
355
+ *
356
+ * @example
357
+ * ```tsx
358
+ * <input aria-invalid={shouldShowError || undefined} />
359
+ * {shouldShowError && <p id={errorId} role="alert">{error?.message}</p>}
360
+ * ```
361
+ */
362
+ shouldShowError: boolean;
222
363
  /** Whether dynamic options are currently loading */
223
364
  isLoading?: boolean;
365
+ /**
366
+ * Pre-computed react-hook-form validation rules for this field.
367
+ * Equivalent to calling `buildValidationRules(field)` — provided here so
368
+ * adapter components can pass `rules={rules}` directly to `<Controller>`
369
+ * without importing or calling `buildValidationRules` themselves.
370
+ *
371
+ * @example
372
+ * ```tsx
373
+ * function TextInput({ field, control, rules }: FieldComponentProps) {
374
+ * return (
375
+ * <Controller name={field.name} control={control} rules={rules} render={...} />
376
+ * );
377
+ * }
378
+ * ```
379
+ */
380
+ rules: ValidationRules;
224
381
  }
225
382
  /**
226
383
  * Field component type.
@@ -271,6 +428,17 @@ interface Section<TFieldValues extends FieldValues = FieldValues> {
271
428
  collapsible?: boolean;
272
429
  /** Default collapsed state */
273
430
  defaultCollapsed?: boolean;
431
+ /**
432
+ * Defer rendering of this section until it scrolls near the viewport.
433
+ * Applies `content-visibility: auto` + `contain-intrinsic-size` to the
434
+ * section container, skipping layout/paint work while off-screen.
435
+ *
436
+ * **Only use for sections that are below the initial fold.** Applying this
437
+ * to above-fold sections has no benefit and slightly increases overhead.
438
+ *
439
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility
440
+ */
441
+ deferRender?: boolean;
274
442
  }
275
443
  /**
276
444
  * Props passed to section render function.
@@ -298,6 +466,12 @@ interface SectionLayoutProps {
298
466
  collapsible?: boolean;
299
467
  /** Default collapsed state */
300
468
  defaultCollapsed?: boolean;
469
+ /**
470
+ * When true the section is below the initial fold and should defer
471
+ * browser layout/paint work until it nears the viewport.
472
+ * Layout components may apply `content-visibility: auto` here.
473
+ */
474
+ deferRender?: boolean;
301
475
  /** Children content */
302
476
  children: ReactNode;
303
477
  }
@@ -336,6 +510,12 @@ type LayoutComponent<TProps extends LayoutComponentProps = LayoutComponentProps>
336
510
  * @template TFieldValues - Form field values type for type-safe schemas
337
511
  */
338
512
  interface FormSchema<TFieldValues extends FieldValues = FieldValues> {
513
+ /** Optional schema identifier — useful for serialization, analytics, and AI context */
514
+ id?: string;
515
+ /** Human-readable form title (AI / documentation use) */
516
+ title?: string;
517
+ /** Human-readable description of the form's purpose */
518
+ description?: string;
339
519
  /** Form sections */
340
520
  sections: Section<TFieldValues>[];
341
521
  }
@@ -462,10 +642,12 @@ interface SectionRendererProps<TFieldValues extends FieldValues = FieldValues> {
462
642
  disabled?: boolean;
463
643
  variant?: Variant;
464
644
  }
645
+ declare function SectionRendererImpl<TFieldValues extends FieldValues = FieldValues>(props: SectionRendererProps<TFieldValues>): FormElement;
465
646
  /**
466
- * Renders a single section with its fields.
647
+ * Memoized section renderer. Re-renders only when section config or
648
+ * disabled/variant context changes — not on every form value change.
467
649
  */
468
- declare function SectionRenderer<TFieldValues extends FieldValues = FieldValues>(props: SectionRendererProps<TFieldValues>): FormElement;
650
+ declare const SectionRenderer: typeof SectionRendererImpl;
469
651
  interface GridRendererProps<TFieldValues extends FieldValues = FieldValues> {
470
652
  fields?: BaseField<TFieldValues>[];
471
653
  cols?: number;
@@ -474,10 +656,7 @@ interface GridRendererProps<TFieldValues extends FieldValues = FieldValues> {
474
656
  disabled?: boolean;
475
657
  variant?: Variant;
476
658
  }
477
- /**
478
- * Renders a grid of fields with specified column layout.
479
- */
480
- declare function GridRenderer<TFieldValues extends FieldValues = FieldValues>({
659
+ declare function GridRendererImpl<TFieldValues extends FieldValues = FieldValues>({
481
660
  fields,
482
661
  cols,
483
662
  gap,
@@ -485,18 +664,22 @@ declare function GridRenderer<TFieldValues extends FieldValues = FieldValues>({
485
664
  disabled,
486
665
  variant
487
666
  }: GridRendererProps<TFieldValues>): FormElement;
667
+ /**
668
+ * Memoized grid renderer.
669
+ */
670
+ declare const GridRenderer: typeof GridRendererImpl;
488
671
  interface FieldWrapperProps<TFieldValues extends FieldValues = FieldValues> {
489
672
  field: BaseField<TFieldValues>;
490
673
  control?: Control<TFieldValues>;
491
674
  disabled?: boolean;
492
675
  variant?: Variant;
493
676
  }
677
+ declare function FieldWrapperImpl<TFieldValues extends FieldValues = FieldValues>(props: FieldWrapperProps<TFieldValues>): FormElement;
494
678
  /**
495
- * Wraps individual fields.
496
- * If the field requires conditional logic or dynamic options, it uses the Dynamic wrapper.
497
- * Otherwise, it uses the Static wrapper, vastly improving performance by skipping `useWatch`.
679
+ * Memoized field wrapper — the dispatch layer between schema fields and
680
+ * their renderer. Re-renders only when the field config itself changes.
498
681
  */
499
- declare function FieldWrapper<TFieldValues extends FieldValues = FieldValues>(props: FieldWrapperProps<TFieldValues>): FormElement;
682
+ declare const FieldWrapper: typeof FieldWrapperImpl;
500
683
  //#endregion
501
684
  //#region src/FormSystemContext.d.ts
502
685
  /**
@@ -645,6 +828,7 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
645
828
  /**
646
829
  * Extracts default values from a form schema.
647
830
  * Walks all sections and fields, respecting nameSpace prefixes and group nesting.
831
+ * Array fields default to `[]` when no explicit `defaultValue` is provided.
648
832
  *
649
833
  * @example
650
834
  * ```ts
@@ -653,11 +837,30 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
653
837
  * ```
654
838
  */
655
839
  declare function extractDefaultValues<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Partial<TFieldValues>;
840
+ /**
841
+ * Async version of `extractDefaultValues` for large schemas (50+ fields).
842
+ * Yields to the main thread after each section so that the browser can
843
+ * handle input events between chunks — keeping INP scores low.
844
+ *
845
+ * Use in `getDefaultValues` passed to `useForm` when the schema is known
846
+ * to be large:
847
+ *
848
+ * @example
849
+ * ```ts
850
+ * const form = useForm({
851
+ * defaultValues: async () => extractDefaultValuesAsync(schema),
852
+ * });
853
+ * ```
854
+ */
855
+ declare function extractDefaultValuesAsync<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Promise<Partial<TFieldValues>>;
656
856
  /**
657
857
  * Generates react-hook-form `RegisterOptions`-compatible validation rules
658
858
  * from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
659
859
  * `maxLength`, `pattern`, and `validate` to RHF rules.
660
860
  *
861
+ * Supports both shorthand scalars and `{ value, message }` objects for all
862
+ * numeric/length rules, and `{ regex, message }` for pattern.
863
+ *
661
864
  * @example
662
865
  * ```tsx
663
866
  * import { buildValidationRules } from '@classytic/formkit';
@@ -669,175 +872,171 @@ declare function extractDefaultValues<TFieldValues extends FieldValues = FieldVa
669
872
  * ```
670
873
  */
671
874
  declare function buildValidationRules<TFieldValues extends FieldValues = FieldValues>(field: BaseField<TFieldValues>): ValidationRules;
875
+ /** Returns true for fields that carry an `options` array (select, radio, etc.) */
876
+ declare function isChoiceField(field: BaseField): boolean;
877
+ /** Returns true for free-text input fields */
878
+ declare function isTextField(field: BaseField): boolean;
879
+ /** Returns true for numeric input fields */
880
+ declare function isNumericField(field: BaseField): boolean;
881
+ /** Returns true for date / time fields */
882
+ declare function isDateField(field: BaseField): boolean;
883
+ /** Returns true for structural fields that contain sub-fields (`itemFields`) */
884
+ declare function isContainerField(field: BaseField): boolean;
885
+ /** Returns true for array fields that render a repeatable list */
886
+ declare function isArrayField(field: BaseField): boolean;
887
+ /** Returns true for fields that load options asynchronously */
888
+ declare function isDynamicField(field: BaseField): boolean;
889
+ /** Returns true for fields with conditional rendering */
890
+ declare function isConditionalField(field: BaseField): boolean;
891
+ /**
892
+ * Merge two or more schemas into one, concatenating their sections.
893
+ *
894
+ * @example
895
+ * ```ts
896
+ * const full = mergeSchemas(personalSchema, addressSchema, billingSchema);
897
+ * ```
898
+ */
899
+ declare function mergeSchemas<TFieldValues extends FieldValues = FieldValues>(...schemas: FormSchema<TFieldValues>[]): FormSchema<TFieldValues>;
900
+ /**
901
+ * Add fields to a section identified by `sectionId`.
902
+ * Returns a new schema — the original is not mutated.
903
+ *
904
+ * @example
905
+ * ```ts
906
+ * const extended = extendSection(schema, "personal", [
907
+ * field.text("middleName", "Middle Name"),
908
+ * ]);
909
+ * ```
910
+ */
911
+ declare function extendSection<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, sectionId: string, fields: BaseField<TFieldValues>[], position?: "start" | "end"): FormSchema<TFieldValues>;
912
+ /**
913
+ * Create a new schema that includes only the named fields.
914
+ *
915
+ * @example
916
+ * ```ts
917
+ * const slim = pickFields(schema, ["email", "password"]);
918
+ * ```
919
+ */
920
+ declare function pickFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
921
+ /**
922
+ * Create a new schema that excludes the named fields.
923
+ *
924
+ * @example
925
+ * ```ts
926
+ * const withoutInternal = omitFields(schema, ["__id", "__createdAt"]);
927
+ * ```
928
+ */
929
+ declare function omitFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
930
+ /**
931
+ * Collect every field from every section into a flat array.
932
+ * Useful for validation, documentation, and AI schema introspection.
933
+ *
934
+ * @example
935
+ * ```ts
936
+ * const allFields = flattenSchema(schema);
937
+ * const required = allFields.filter(f => f.required);
938
+ * ```
939
+ */
940
+ declare function flattenSchema<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): BaseField<TFieldValues>[];
941
+ /**
942
+ * Maps a server error response to react-hook-form field errors.
943
+ *
944
+ * Call this in your `onError` / `catch` handler after a failed API submission
945
+ * to surface per-field server-side errors using the same UX as client validation.
946
+ *
947
+ * @param form - The `useForm` return value
948
+ * @param errors - Map of field path → error message (dot-notation paths supported)
949
+ *
950
+ * @example
951
+ * ```ts
952
+ * async function onSubmit(data: FormValues) {
953
+ * try {
954
+ * await api.save(data);
955
+ * } catch (err) {
956
+ * if (err.fieldErrors) {
957
+ * applyServerErrors(form, err.fieldErrors);
958
+ * // { email: "Already taken", "address.zip": "Invalid ZIP" }
959
+ * }
960
+ * }
961
+ * }
962
+ * ```
963
+ */
964
+ declare function applyServerErrors<TFieldValues extends FieldValues = FieldValues>(form: UseFormReturn<TFieldValues>, errors: Record<string, string>): void;
672
965
  //#endregion
673
966
  //#region src/builders.d.ts
674
967
  /**
675
- * Additional field props for builder helpers.
968
+ * Additional field props accepted by builder helpers.
676
969
  * Accepts all BaseField properties except `name`, `type`, and `label`
677
- * which are set by the builder method.
970
+ * which are provided by the builder method directly.
678
971
  */
679
- type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label"> & {
680
- /** Grid column class (e.g., "col-span-2") */gridColumn?: string; /** Icon for the left side of input */
681
- iconLeft?: ReactNode; /** Icon for the right side of input */
682
- iconRight?: ReactNode; /** Additional custom props */
683
- [key: string]: unknown;
684
- };
972
+ type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label">;
685
973
  /**
686
974
  * Section configuration props.
687
975
  */
688
976
  interface SectionProps<TFieldValues extends FieldValues = FieldValues> extends Omit<Section<TFieldValues>, "id" | "title" | "fields" | "cols"> {
689
977
  cols?: number;
690
978
  }
691
- /**
692
- * Render function for custom field types.
693
- */
694
- type CustomRenderFn = (props: {
695
- control: Control<FieldValues>;
696
- disabled?: boolean;
697
- error?: FieldError;
698
- }) => ReactNode;
699
979
  /**
700
980
  * Type-safe field builder helpers for schema-driven forms.
701
981
  *
702
- * Provides shorthand methods for common field types with sensible defaults,
703
- * reducing boilerplate while maintaining full type safety.
982
+ * All methods are generic over TFieldValues, defaulting to FieldValues (any string)
983
+ * when no type argument is provided. Specify the generic to enforce that field
984
+ * names are valid paths in your form values type.
985
+ *
986
+ * For fully-typed schemas where every field name is checked, prefer
987
+ * `field.for<MyForm>()` which fixes the generic once for the whole schema:
704
988
  *
705
989
  * @example
706
990
  * ```ts
707
- * import { field, section } from '@classytic/formkit';
708
- *
709
- * const schema = {
710
- * sections: [
711
- * section("personal", "Personal Info", [
712
- * field.text("firstName", "First Name", { required: true }),
713
- * field.email("email", "Email"),
714
- * field.select("role", "Role", [
715
- * { label: "Admin", value: "admin" },
716
- * { label: "User", value: "user" },
717
- * ]),
718
- * ], { cols: 2 }),
719
- * ],
720
- * };
991
+ * // Untyped any string accepted (backwards compatible)
992
+ * field.text("email", "Email")
993
+ *
994
+ * // Per-call generic — name is checked against MyForm
995
+ * field.text<MyForm>("email", "Email")
996
+ *
997
+ * // Typed factory — name checked on every call without repeating the generic
998
+ * const f = field.for<MyForm>()
999
+ * f.text("email", "Email") //
1000
+ * f.text("typo", "Email") // ✗ TypeScript error
721
1001
  * ```
722
1002
  */
723
1003
  declare const field: {
724
- /**
725
- * Text input field.
726
- */
727
- text: (name: string, label: string, props?: FieldProps) => BaseField;
728
- /**
729
- * Email input field with default placeholder.
730
- */
731
- email: (name: string, label: string, props?: FieldProps) => BaseField;
732
- /**
733
- * URL input field with default placeholder.
734
- */
735
- url: (name: string, label: string, props?: FieldProps) => BaseField;
736
- /**
737
- * Phone/tel input field with default placeholder.
738
- */
739
- tel: (name: string, label: string, props?: FieldProps) => BaseField;
740
- /**
741
- * Password input field.
742
- */
743
- password: (name: string, label: string, props?: FieldProps) => BaseField;
744
- /**
745
- * Number input field with min: 0 default.
746
- */
747
- number: (name: string, label: string, props?: FieldProps) => BaseField;
748
- /**
749
- * Textarea field with default 3 rows.
750
- */
751
- textarea: (name: string, label: string, props?: FieldProps) => BaseField;
752
- /**
753
- * Select dropdown field.
754
- */
755
- select: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
756
- /**
757
- * Searchable combobox field.
758
- */
759
- combobox: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
760
- /**
761
- * Multi-select field (tag choice).
762
- */
763
- multiselect: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
764
- /**
765
- * Tag choice field for selecting options as tags/chips.
766
- */
767
- tagChoice: (name: string, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps) => BaseField;
768
- /**
769
- * Dependent select field that reacts to parent field changes.
770
- */
771
- dependentSelect: (name: string, label: string, props?: FieldProps) => BaseField;
772
- /**
773
- * Switch/toggle field.
774
- */
775
- switch: (name: string, label: string, props?: FieldProps) => BaseField;
776
- /**
777
- * Boolean field (alias for switch).
778
- */
779
- boolean: (name: string, label: string, props?: FieldProps) => BaseField;
780
- /**
781
- * Checkbox field.
782
- */
783
- checkbox: (name: string, label: string, props?: FieldProps) => BaseField;
784
- /**
785
- * Radio button group field.
786
- */
787
- radio: (name: string, label: string, options: FieldOption[], props?: FieldProps) => BaseField;
788
- /**
789
- * Date picker field.
790
- */
791
- date: (name: string, label: string, props?: FieldProps) => BaseField;
792
- /**
793
- * Tag input field with default placeholder.
794
- */
795
- tags: (name: string, label: string, props?: FieldProps) => BaseField;
796
- /**
797
- * Slug field with auto-generation from source value.
798
- */
799
- slug: (name: string, label: string, props?: FieldProps) => BaseField;
800
- /**
801
- * File upload field.
802
- */
803
- file: (name: string, label: string, props?: FieldProps) => BaseField;
804
- /**
805
- * OTP/PIN input field.
806
- */
807
- otp: (name: string, label: string, props?: FieldProps) => BaseField;
808
- /**
809
- * Async searchable combobox with server-side search.
810
- */
811
- asyncCombobox: (name: string, label: string, props?: FieldProps) => BaseField;
812
- /**
813
- * Async searchable multi-select with server-side search.
814
- */
815
- asyncMultiselect: (name: string, label: string, props?: FieldProps) => BaseField;
816
- /**
817
- * Date and optional time picker field.
818
- */
819
- dateTime: (name: string, label: string, props?: FieldProps) => BaseField;
820
- /**
821
- * Hidden field (no UI).
822
- */
823
- hidden: (name: string, props?: FieldProps) => BaseField;
1004
+ /** Text input field. */text: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Email input field with default placeholder. */
1005
+ email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
1006
+ url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
1007
+ tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
1008
+ 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). */
1009
+ number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
1010
+ textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
1011
+ select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
1012
+ combobox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Multi-select field. */
1013
+ 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. */
1014
+ dependentSelect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Switch/toggle field. */
1015
+ switch: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Boolean field (alias for switch). */
1016
+ boolean: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Checkbox field. */
1017
+ checkbox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Radio button group field. */
1018
+ radio: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>; /** Date picker field. */
1019
+ date: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Tag input field. */
1020
+ tags: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Slug field. */
1021
+ slug: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** File upload field. */
1022
+ file: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Hidden field (no UI). */
1023
+ hidden: <T extends FieldValues = FieldValues>(name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
824
1024
  /**
825
1025
  * Group field for nested objects.
826
- * Renders itemFields as a sub-grid within the form.
1026
+ * Renders itemFields as a sub-grid. Child names are relative (e.g. "street"),
1027
+ * FormGenerator prefixes them with the group name at render time.
827
1028
  *
828
1029
  * @example
829
1030
  * ```ts
830
1031
  * field.group("address", "Address", [
831
1032
  * field.text("street", "Street"),
832
1033
  * field.text("city", "City"),
833
- * field.text("zip", "ZIP Code"),
834
- * ], { cols: 3 })
1034
+ * ], { cols: 2 })
835
1035
  * ```
836
1036
  */
837
- group: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
1037
+ group: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
838
1038
  /**
839
- * Array/repeatable field.
840
- * Renders a dynamic list of sub-forms using react-hook-form's useFieldArray.
1039
+ * Array/repeatable field backed by react-hook-form's useFieldArray.
841
1040
  *
842
1041
  * @example
843
1042
  * ```ts
@@ -847,42 +1046,109 @@ declare const field: {
847
1046
  * ])
848
1047
  * ```
849
1048
  */
850
- array: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
1049
+ array: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
851
1050
  /**
852
1051
  * Custom field with a render function.
853
- * Bypasses the component registry entirely.
1052
+ * Bypasses the component registry — full control over rendering.
1053
+ *
1054
+ * The render callback receives the complete `FieldComponentProps` including
1055
+ * `fieldId`, `errorId`, `shouldShowError`, `error`, `rules`, and `control`.
1056
+ *
1057
+ * Use `shouldShowError` (not `!!error`) to drive `aria-invalid` and error
1058
+ * visibility so timing mirrors the CSS `:user-invalid` pseudo-class.
854
1059
  *
855
1060
  * @example
856
- * ```ts
857
- * field.custom("skills", "Skills", ({ control, disabled }) => (
858
- * <SkillSelector control={control} disabled={disabled} />
1061
+ * ```tsx
1062
+ * field.custom("skills", "Skills", ({ control, shouldShowError, errorId, error, fieldId }) => (
1063
+ * <div>
1064
+ * <SkillSelector
1065
+ * id={fieldId}
1066
+ * control={control}
1067
+ * aria-invalid={shouldShowError || undefined}
1068
+ * aria-errormessage={shouldShowError ? errorId : undefined}
1069
+ * />
1070
+ * {shouldShowError && (
1071
+ * <p id={errorId} role="alert" className="text-sm text-destructive">
1072
+ * {error?.message}
1073
+ * </p>
1074
+ * )}
1075
+ * </div>
859
1076
  * ))
860
1077
  * ```
861
1078
  */
862
- custom: (name: string, label: string, render: CustomRenderFn, props?: FieldProps) => BaseField;
1079
+ custom: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, props?: FieldProps<T>) => BaseField<T>;
1080
+ /**
1081
+ * Returns a typed field builder with `TFieldValues` fixed.
1082
+ * Every field name is validated against `Path<TFieldValues>` at the call site —
1083
+ * no need to repeat the generic on each individual builder call.
1084
+ *
1085
+ * @example
1086
+ * ```ts
1087
+ * interface ContactForm {
1088
+ * firstName: string;
1089
+ * email: string;
1090
+ * address: { street: string; city: string };
1091
+ * }
1092
+ *
1093
+ * const f = field.for<ContactForm>()
1094
+ *
1095
+ * const schema = defineSchema<ContactForm>({
1096
+ * sections: [{
1097
+ * fields: [
1098
+ * f.text("firstName", "First Name"), // ✓
1099
+ * f.email("email", "Email"), // ✓
1100
+ * f.text("typo", "Label"), // ✗ TypeScript error
1101
+ * ],
1102
+ * }],
1103
+ * })
1104
+ * ```
1105
+ */
1106
+ for: <T extends FieldValues>() => {
1107
+ text: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1108
+ email: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1109
+ url: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1110
+ tel: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1111
+ password: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1112
+ number: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1113
+ textarea: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1114
+ select: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
1115
+ combobox: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
1116
+ multiselect: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
1117
+ dependentSelect: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1118
+ switch: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1119
+ boolean: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1120
+ checkbox: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1121
+ radio: (name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>;
1122
+ date: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1123
+ tags: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1124
+ slug: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1125
+ file: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1126
+ hidden: (name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
1127
+ group: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
1128
+ array: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
1129
+ custom: (name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, builderProps?: FieldProps<T>) => BaseField<T>;
1130
+ };
863
1131
  };
864
1132
  /**
865
1133
  * Create a section definition with sensible defaults.
866
1134
  *
867
- * @param id - Unique section identifier
868
- * @param title - Section title
869
- * @param fields - Array of field definitions
870
- * @param props - Additional section configuration
871
- *
872
1135
  * @example
873
1136
  * ```ts
874
1137
  * section("personal", "Personal Info", [
875
1138
  * field.text("name", "Name", { required: true }),
876
1139
  * field.email("email", "Email"),
877
- * ], { cols: 2, variant: "card" })
1140
+ * ], { cols: 2 })
878
1141
  * ```
879
1142
  */
880
1143
  declare function section<TFieldValues extends FieldValues = FieldValues>(id: string, title: string, fields: BaseField<TFieldValues>[], props?: SectionProps<TFieldValues>): Section<TFieldValues>;
881
1144
  /**
882
1145
  * Create a section without a title (transparent section).
883
1146
  * Useful for grouping fields without visual separation.
1147
+ *
1148
+ * Accepts `BaseField[]` (no generic) so mixed-type field arrays don't trigger
1149
+ * conflicting type inference across different field name generics.
884
1150
  */
885
- declare function sectionUntitled<TFieldValues extends FieldValues = FieldValues>(fields: BaseField<TFieldValues>[], props?: Omit<SectionProps<TFieldValues>, "variant">): Section<TFieldValues>;
1151
+ declare function sectionUntitled(fields: BaseField[], props?: Omit<SectionProps, "variant">): Section;
886
1152
  //#endregion
887
1153
  //#region src/useFormKit.d.ts
888
1154
  /**
@@ -930,4 +1196,4 @@ interface UseFormKitReturn<TFieldValues extends FieldValues = FieldValues> exten
930
1196
  */
931
1197
  declare function useFormKit<TFieldValues extends FieldValues = FieldValues>(options: UseFormKitOptions<TFieldValues>): UseFormKitReturn<TFieldValues>;
932
1198
  //#endregion
933
- export { type BaseField, type ClassValue, type ComponentRegistry, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldComponent, type FieldComponentProps, type FieldOption, type FieldOptionGroup, type FieldType, FieldWrapper, type FormElement, FormGenerator, type FormGeneratorProps, type FormSchema, type FormSystemContextValue, FormSystemProvider, type FormSystemProviderProps, type GridLayoutProps, GridRenderer, type InferSchemaValues, type LayoutComponent, type LayoutComponentProps, type LayoutRegistry, type LayoutType, type SchemaFieldNames, type Section, type SectionLayoutProps, type SectionRenderProps, SectionRenderer, type UseFormKitOptions, type UseFormKitReturn, type ValidationRules, type Variant, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extractDefaultValues, extractWatchNames, field, section, sectionUntitled, shallowEqual, useFieldComponent, useFormKit, useFormSystem, useLayoutComponent };
1199
+ export { type BaseField, type ClassValue, type ComponentRegistry, type Condition, type ConditionConfig, type ConditionRule, type DefaultLayoutProps, type DefineField, type FieldComponent, type FieldComponentProps, type FieldMeta, type FieldOption, type FieldOptionGroup, type FieldType, FieldWrapper, type FieldWrapperProps, type FormElement, FormGenerator, type FormGeneratorProps, type FormSchema, type FormSystemContextValue, FormSystemProvider, type FormSystemProviderProps, type GridLayoutProps, GridRenderer, type GridRendererProps, type InferSchemaValues, type LayoutComponent, type LayoutComponentProps, type LayoutRegistry, type LayoutType, type PatternRuleObject, type SchemaFieldNames, type Section, type SectionLayoutProps, type SectionRenderProps, SectionRenderer, type SectionRendererProps, type UseFormKitOptions, type UseFormKitReturn, type ValidationRuleObject, type ValidationRules, type Variant, applyServerErrors, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractDefaultValuesAsync, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeSchemas, omitFields, pickFields, section, sectionUntitled, shallowEqual, useFieldComponent, useFormKit, useFormSystem, useLayoutComponent };