@classytic/formkit 1.3.1 → 1.5.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;
@@ -118,6 +161,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
118
161
  variant?: Variant;
119
162
  /** Whether field should span full width in grid */
120
163
  fullWidth?: boolean;
164
+ /**
165
+ * Span N columns of the section grid (e.g. `2` of a 3-col section). `1` (or
166
+ * unset) is the default single cell; `fullWidth` still means "span the whole
167
+ * row" and takes precedence. Applied as an inline `grid-column: span N` so it
168
+ * works regardless of the host's Tailwind content scan.
169
+ */
170
+ colSpan?: number;
121
171
  /** Custom CSS class name */
122
172
  className?: string;
123
173
  /**
@@ -129,18 +179,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
129
179
  defaultValue?: unknown;
130
180
  /** Options for select/radio/checkbox fields */
131
181
  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;
182
+ /**
183
+ * Minimum value (for number/date fields).
184
+ * Pass an object `{ value, message }` to provide a custom error message.
185
+ */
186
+ min?: number | string | ValidationRuleObject<number | string>;
187
+ /**
188
+ * Maximum value (for number/date fields).
189
+ * Pass an object `{ value, message }` to provide a custom error message.
190
+ */
191
+ max?: number | string | ValidationRuleObject<number | string>;
136
192
  /** Step value (for number fields) */
137
193
  step?: number;
138
- /** Pattern for validation (regex string) */
139
- pattern?: string;
140
- /** Minimum length */
141
- minLength?: number;
142
- /** Maximum length */
143
- maxLength?: number;
194
+ /**
195
+ * Pattern for validation.
196
+ * Pass a regex string or `{ regex, message }` for a custom error message.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * // simple
201
+ * pattern: "^[a-z]+$"
202
+ * // with custom message
203
+ * pattern: { regex: "^[a-z]+$", message: "Only lowercase letters allowed" }
204
+ * ```
205
+ */
206
+ pattern?: string | PatternRuleObject;
207
+ /**
208
+ * Minimum length.
209
+ * Pass an object `{ value, message }` to provide a custom error message.
210
+ */
211
+ minLength?: number | ValidationRuleObject<number>;
212
+ /**
213
+ * Maximum length.
214
+ * Pass an object `{ value, message }` to provide a custom error message.
215
+ */
216
+ maxLength?: number | ValidationRuleObject<number>;
144
217
  /** Number of rows (for textarea) */
145
218
  rows?: number;
146
219
  /** Multiple selection (for select/file) */
@@ -156,18 +229,41 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
156
229
  /**
157
230
  * Dynamic options loaded based on current form values.
158
231
  * Useful for dependent selects (e.g., state depends on country).
232
+ *
233
+ * Receives an `AbortSignal` in the second arg — forward it to `fetch` (or
234
+ * abort your own request on it) so a superseded / unmounted load is cancelled
235
+ * instead of racing to completion:
236
+ *
237
+ * ```ts
238
+ * loadOptions: (values, { signal }) =>
239
+ * fetch(`/api/cities?country=${values.country}`, { signal }).then(r => r.json())
240
+ * ```
159
241
  */
160
- loadOptions?: (formValues: Partial<TFieldValues>) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
242
+ loadOptions?: (formValues: Partial<TFieldValues>, options?: {
243
+ signal: AbortSignal;
244
+ }) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
161
245
  /**
162
246
  * Error callback for loadOptions failures.
163
247
  * Called when loadOptions rejects. Defaults to console.error.
164
248
  */
165
249
  onLoadError?: (error: unknown) => void;
166
250
  /**
167
- * Fields for array or object types.
168
- * Useful for 'array' or 'group' field types that need a sub-schema.
251
+ * Memoize async `loadOptions` results per set of watched values, so toggling
252
+ * a dependency back and forth (e.g. re-selecting a country) reuses the last
253
+ * fetch instead of hitting the API again. Off by default — enable only when
254
+ * the options are stable for a given input, since the cache lives for the
255
+ * field's lifetime and won't see server-side changes. Bounded (LRU-ish) so it
256
+ * can't grow without limit.
257
+ */
258
+ cacheOptions?: boolean;
259
+ /**
260
+ * Sub-fields for `group` and `array` field types.
261
+ *
262
+ * These fields use **relative** names (`"street"`, `"email"`) — FormGenerator
263
+ * prefixes them with the parent field name at render time (`"address.street"`).
264
+ * They are intentionally untyped to TFieldValues for this reason.
169
265
  */
170
- itemFields?: BaseField<TFieldValues>[];
266
+ itemFields?: BaseField[];
171
267
  /**
172
268
  * Custom render function to override the component registry for this specific field.
173
269
  * Completely bypasses the globally registered FieldComponent for this type.
@@ -176,15 +272,22 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
176
272
  /**
177
273
  * Cross-field validation function.
178
274
  * Receives the field value and all form values for cross-field checks.
179
- * Return `true` for valid, or a string error message for invalid.
275
+ * Return `true` for valid, a string error message for invalid, or a Promise of either
276
+ * for async validation (e.g., checking server-side uniqueness).
180
277
  *
181
278
  * @example
182
279
  * ```ts
183
280
  * validate: (value, formValues) =>
184
281
  * value > formValues.minPrice || "Must be greater than min price"
282
+ *
283
+ * // Async example
284
+ * validate: async (value) => {
285
+ * const taken = await checkUsernameAvailability(value as string);
286
+ * return taken ? "Username already taken" : true;
287
+ * }
185
288
  * ```
186
289
  */
187
- validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true;
290
+ validate?: (value: unknown, formValues: Partial<TFieldValues>) => string | true | Promise<string | true>;
188
291
  /**
189
292
  * Dependencies for optimizing conditionally rendered fields.
190
293
  * Allows specifying specific field names to watch, preventing full form re-renders.
@@ -192,6 +295,32 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
192
295
  watchNames?: Path<TFieldValues> | Path<TFieldValues>[];
193
296
  /** Additional field-specific props for custom components */
194
297
  customProps?: Record<string, unknown>;
298
+ /**
299
+ * AI / agent-friendly metadata for this field.
300
+ *
301
+ * Provides hints that help LLM coding assistants generate correct field
302
+ * configs without needing to inspect the schema at runtime.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * field.text("companyName", "Company", {
307
+ * meta: {
308
+ * description: "Legal entity name of the organization",
309
+ * example: "Acme Corp",
310
+ * category: "identity",
311
+ * tags: ["crm", "required-for-billing"],
312
+ * }
313
+ * })
314
+ * ```
315
+ */
316
+ meta?: FieldMeta;
317
+ /**
318
+ * Escape hatch for adapter-specific or custom props.
319
+ * Allows passing arbitrary props directly on the field object so they
320
+ * flow through the adapter spread (`{...field}`) without needing `customProps`.
321
+ * Intentionally broad to support diverse UI component libraries.
322
+ */
323
+ [key: string]: unknown;
195
324
  }
196
325
  /**
197
326
  * Props passed to field components.
@@ -237,13 +366,68 @@ interface FieldComponentProps<TFieldValues extends FieldValues = FieldValues> ex
237
366
  invalid: boolean;
238
367
  isDirty: boolean;
239
368
  isTouched: boolean;
240
- isValidating: boolean;
369
+ isValidating: boolean; /** True after the enclosing form's submit handler has been called at least once. */
370
+ isSubmitted: boolean;
241
371
  error?: FieldError;
242
372
  };
243
- /** Generated field ID for label-input association (e.g. `formkit-field-email`) */
373
+ /**
374
+ * Generated field ID for label-input association (e.g. `formkit-field-email`).
375
+ * Use as `id` on the input element and `htmlFor` on the `<label>`.
376
+ */
244
377
  fieldId: string;
378
+ /**
379
+ * Generated error container ID for ARIA association (e.g. `formkit-field-email-error`).
380
+ * Use as `id` on the error message element and as the value for `aria-errormessage`
381
+ * (preferred) or `aria-describedby` (broader support) on the input.
382
+ *
383
+ * @example
384
+ * ```tsx
385
+ * <input
386
+ * id={fieldId}
387
+ * aria-invalid={shouldShowError || undefined}
388
+ * aria-errormessage={shouldShowError ? errorId : undefined}
389
+ * />
390
+ * <p id={errorId} role="alert" aria-live="polite">
391
+ * {shouldShowError ? error?.message : null}
392
+ * </p>
393
+ * ```
394
+ */
395
+ errorId: string;
396
+ /**
397
+ * Whether to display the field error right now.
398
+ *
399
+ * Aligns with the CSS `:user-invalid` timing model: `true` only after the
400
+ * user has interacted with the field (blur) **or** the form has been
401
+ * submitted. This prevents premature "required" errors on untouched fields.
402
+ *
403
+ * Use this — not `!!error` — to drive `aria-invalid`, error message
404
+ * visibility, and destructive ring/border styles.
405
+ *
406
+ * @example
407
+ * ```tsx
408
+ * <input aria-invalid={shouldShowError || undefined} />
409
+ * {shouldShowError && <p id={errorId} role="alert">{error?.message}</p>}
410
+ * ```
411
+ */
412
+ shouldShowError: boolean;
245
413
  /** Whether dynamic options are currently loading */
246
414
  isLoading?: boolean;
415
+ /**
416
+ * Pre-computed react-hook-form validation rules for this field.
417
+ * Equivalent to calling `buildValidationRules(field)` — provided here so
418
+ * adapter components can pass `rules={rules}` directly to `<Controller>`
419
+ * without importing or calling `buildValidationRules` themselves.
420
+ *
421
+ * @example
422
+ * ```tsx
423
+ * function TextInput({ field, control, rules }: FieldComponentProps) {
424
+ * return (
425
+ * <Controller name={field.name} control={control} rules={rules} render={...} />
426
+ * );
427
+ * }
428
+ * ```
429
+ */
430
+ rules: ValidationRules;
247
431
  }
248
432
  /**
249
433
  * Validation rules compatible with react-hook-form's RegisterOptions.
@@ -289,6 +473,17 @@ interface Section<TFieldValues extends FieldValues = FieldValues> {
289
473
  collapsible?: boolean;
290
474
  /** Default collapsed state */
291
475
  defaultCollapsed?: boolean;
476
+ /**
477
+ * Defer rendering of this section until it scrolls near the viewport.
478
+ * Applies `content-visibility: auto` + `contain-intrinsic-size` to the
479
+ * section container, skipping layout/paint work while off-screen.
480
+ *
481
+ * **Only use for sections that are below the initial fold.** Applying this
482
+ * to above-fold sections has no benefit and slightly increases overhead.
483
+ *
484
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility
485
+ */
486
+ deferRender?: boolean;
292
487
  }
293
488
  /**
294
489
  * Props passed to section render function.
@@ -316,6 +511,12 @@ interface SectionLayoutProps {
316
511
  collapsible?: boolean;
317
512
  /** Default collapsed state */
318
513
  defaultCollapsed?: boolean;
514
+ /**
515
+ * When true the section is below the initial fold and should defer
516
+ * browser layout/paint work until it nears the viewport.
517
+ * Layout components may apply `content-visibility: auto` here.
518
+ */
519
+ deferRender?: boolean;
319
520
  /** Children content */
320
521
  children: ReactNode;
321
522
  }
@@ -350,6 +551,12 @@ type LayoutComponentProps = SectionLayoutProps | GridLayoutProps | DefaultLayout
350
551
  * @template TFieldValues - Form field values type for type-safe schemas
351
552
  */
352
553
  interface FormSchema<TFieldValues extends FieldValues = FieldValues> {
554
+ /** Optional schema identifier — useful for serialization, analytics, and AI context */
555
+ id?: string;
556
+ /** Human-readable form title (AI / documentation use) */
557
+ title?: string;
558
+ /** Human-readable description of the form's purpose */
559
+ description?: string;
353
560
  /** Form sections */
354
561
  sections: Section<TFieldValues>[];
355
562
  }
@@ -402,6 +609,7 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
402
609
  /**
403
610
  * Extracts default values from a form schema.
404
611
  * Walks all sections and fields, respecting nameSpace prefixes and group nesting.
612
+ * Array fields default to `[]` when no explicit `defaultValue` is provided.
405
613
  *
406
614
  * @example
407
615
  * ```ts
@@ -410,11 +618,62 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
410
618
  * ```
411
619
  */
412
620
  declare function extractDefaultValues<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Partial<TFieldValues>;
621
+ interface SchemaIssue {
622
+ /** Where the issue is, e.g. `sections[0].fields[2]` or `sections[1]`. */
623
+ path: string;
624
+ code: "missing-name" | "missing-type" | "duplicate-name" | "itemfields-on-noncontainer" | "empty-container" | "unknown-operator";
625
+ /** `error` = will misbehave at runtime; `warning` = suspicious but tolerated. */
626
+ severity: "error" | "warning";
627
+ message: string;
628
+ }
629
+ /**
630
+ * Structurally validate a form schema and return a list of issues (empty ⇒ OK).
631
+ * Server-safe (no hooks/DOM), so you can run it when a schema is loaded from a
632
+ * DB, in a test, or in a dev boot check. It validates SHAPE, not your component
633
+ * registry — an unknown field `type` is a registry concern, not a schema error.
634
+ *
635
+ * Checks: missing `name`/`type`, duplicate names (namespace-aware), `itemFields`
636
+ * on a non-container type, containers with no `itemFields`, and unknown DSL
637
+ * condition operators.
638
+ */
639
+ declare function validateSchema(schema: FormSchema): SchemaIssue[];
640
+ /**
641
+ * Build a default-value object for a flat list of fields — used to seed a new
642
+ * array item (or a group) from its `itemFields`.
643
+ *
644
+ * Recurses into nested `group` children (→ nested object) and seeds nested
645
+ * `array` children as `[]`, so appending an item never leaves a deep sub-field
646
+ * `undefined`. That matters because a missing deep field can trip a resolver
647
+ * (zod et al.) into a spurious "required" error the moment the row is added.
648
+ * Leaf fields without an explicit `defaultValue` seed to `""` (a controlled
649
+ * empty value RHF is happy with).
650
+ */
651
+ declare function buildFieldDefaults(fields: BaseField[] | undefined): Record<string, unknown>;
652
+ /**
653
+ * Deep-merge `override` onto `base` with default-values semantics: nested
654
+ * plain objects merge recursively; arrays, primitives, and class instances
655
+ * (Date, File, …) from `override` replace wholesale.
656
+ *
657
+ * This is the merge `useFormKit` applies between schema-extracted defaults and
658
+ * caller-provided `defaultValues`. Exported so wrappers that re-seed a form at
659
+ * runtime (e.g. an edit sheet swapping entities) can reproduce the exact same
660
+ * merge for `form.reset(...)`:
661
+ *
662
+ * @example
663
+ * ```ts
664
+ * const merged = mergeDefaultValues(extractDefaultValues(schema), entity);
665
+ * form.reset(merged);
666
+ * ```
667
+ */
668
+ declare function mergeDefaultValues(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown>;
413
669
  /**
414
670
  * Generates react-hook-form `RegisterOptions`-compatible validation rules
415
671
  * from a field's schema props. Maps `required`, `min`, `max`, `minLength`,
416
672
  * `maxLength`, `pattern`, and `validate` to RHF rules.
417
673
  *
674
+ * Supports both shorthand scalars and `{ value, message }` objects for all
675
+ * numeric/length rules, and `{ regex, message }` for pattern.
676
+ *
418
677
  * @example
419
678
  * ```tsx
420
679
  * import { buildValidationRules } from '@classytic/formkit';
@@ -426,155 +685,150 @@ declare function extractDefaultValues<TFieldValues extends FieldValues = FieldVa
426
685
  * ```
427
686
  */
428
687
  declare function buildValidationRules<TFieldValues extends FieldValues = FieldValues>(field: BaseField<TFieldValues>): ValidationRules;
688
+ /** Returns true for fields that carry an `options` array (select, radio, etc.) */
689
+ declare function isChoiceField(field: BaseField): boolean;
690
+ /** Returns true for free-text input fields */
691
+ declare function isTextField(field: BaseField): boolean;
692
+ /** Returns true for numeric input fields */
693
+ declare function isNumericField(field: BaseField): boolean;
694
+ /** Returns true for date / time fields */
695
+ declare function isDateField(field: BaseField): boolean;
696
+ /** Returns true for structural fields that contain sub-fields (`itemFields`) */
697
+ declare function isContainerField(field: BaseField): boolean;
698
+ /** Returns true for array fields that render a repeatable list */
699
+ declare function isArrayField(field: BaseField): boolean;
700
+ /** Returns true for fields that load options asynchronously */
701
+ declare function isDynamicField(field: BaseField): boolean;
702
+ /** Returns true for fields with conditional rendering */
703
+ declare function isConditionalField(field: BaseField): boolean;
704
+ /**
705
+ * Merge two or more schemas into one, concatenating their sections.
706
+ *
707
+ * @example
708
+ * ```ts
709
+ * const full = mergeSchemas(personalSchema, addressSchema, billingSchema);
710
+ * ```
711
+ */
712
+ declare function mergeSchemas<TFieldValues extends FieldValues = FieldValues>(...schemas: FormSchema<TFieldValues>[]): FormSchema<TFieldValues>;
713
+ /**
714
+ * Add fields to a section identified by `sectionId`.
715
+ * Returns a new schema — the original is not mutated.
716
+ *
717
+ * @example
718
+ * ```ts
719
+ * const extended = extendSection(schema, "personal", [
720
+ * field.text("middleName", "Middle Name"),
721
+ * ]);
722
+ * ```
723
+ */
724
+ declare function extendSection<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, sectionId: string, fields: BaseField<TFieldValues>[], position?: "start" | "end"): FormSchema<TFieldValues>;
725
+ /**
726
+ * Create a new schema that includes only the named fields.
727
+ *
728
+ * @example
729
+ * ```ts
730
+ * const slim = pickFields(schema, ["email", "password"]);
731
+ * ```
732
+ */
733
+ declare function pickFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
734
+ /**
735
+ * Create a new schema that excludes the named fields.
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * const withoutInternal = omitFields(schema, ["__id", "__createdAt"]);
740
+ * ```
741
+ */
742
+ declare function omitFields<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>, names: string[]): FormSchema<TFieldValues>;
743
+ /**
744
+ * Collect every field from every section into a flat array.
745
+ * Useful for validation, documentation, and AI schema introspection.
746
+ *
747
+ * @example
748
+ * ```ts
749
+ * const allFields = flattenSchema(schema);
750
+ * const required = allFields.filter(f => f.required);
751
+ * ```
752
+ */
753
+ declare function flattenSchema<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): BaseField<TFieldValues>[];
429
754
  //#endregion
430
755
  //#region src/builders.d.ts
431
756
  /**
432
- * Additional field props for builder helpers.
757
+ * Additional field props accepted by builder helpers.
433
758
  * Accepts all BaseField properties except `name`, `type`, and `label`
434
- * which are set by the builder method.
759
+ * which are provided by the builder method directly.
435
760
  */
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
- };
761
+ type FieldProps<TFieldValues extends FieldValues = FieldValues> = Omit<BaseField<TFieldValues>, "name" | "type" | "label">;
442
762
  /**
443
763
  * Section configuration props.
444
764
  */
445
765
  interface SectionProps<TFieldValues extends FieldValues = FieldValues> extends Omit<Section<TFieldValues>, "id" | "title" | "fields" | "cols"> {
446
766
  cols?: number;
447
767
  }
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
768
  /**
457
769
  * Type-safe field builder helpers for schema-driven forms.
458
770
  *
459
- * Provides shorthand methods for common field types with sensible defaults,
460
- * reducing boilerplate while maintaining full type safety.
771
+ * All methods are generic over TFieldValues, defaulting to FieldValues (any string)
772
+ * when no type argument is provided. Specify the generic to enforce that field
773
+ * names are valid paths in your form values type.
774
+ *
775
+ * For fully-typed schemas where every field name is checked, prefer
776
+ * `field.for<MyForm>()` which fixes the generic once for the whole schema:
461
777
  *
462
778
  * @example
463
779
  * ```ts
464
- * import { field, section } from '@classytic/formkit';
780
+ * // Untyped any string accepted (backwards compatible)
781
+ * field.text("email", "Email")
465
782
  *
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
- * };
783
+ * // Per-call generic — name is checked against MyForm
784
+ * field.text<MyForm>("email", "Email")
785
+ *
786
+ * // Typed factory name checked on every call without repeating the generic
787
+ * const f = field.for<MyForm>()
788
+ * f.text("email", "Email") //
789
+ * f.text("typo", "Email") // ✗ TypeScript error
478
790
  * ```
479
791
  */
480
792
  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
- * Dependent select field that reacts to parent field changes.
523
- */
524
- dependentSelect: (name: string, label: string, props?: FieldProps) => BaseField;
525
- /**
526
- * Switch/toggle field.
527
- */
528
- switch: (name: string, label: string, props?: FieldProps) => BaseField;
529
- /**
530
- * Boolean field (alias for switch).
531
- */
532
- boolean: (name: string, label: string, props?: FieldProps) => BaseField;
533
- /**
534
- * Checkbox field.
535
- */
536
- checkbox: (name: string, label: string, props?: FieldProps) => BaseField;
537
- /**
538
- * Radio button group field.
539
- */
540
- radio: (name: string, label: string, options: FieldOption[], props?: FieldProps) => BaseField;
541
- /**
542
- * Date picker field.
543
- */
544
- date: (name: string, label: string, props?: FieldProps) => BaseField;
545
- /**
546
- * Tag input field with default placeholder.
547
- */
548
- tags: (name: string, label: string, props?: FieldProps) => BaseField;
549
- /**
550
- * Slug field with auto-generation from source value.
551
- */
552
- slug: (name: string, label: string, props?: FieldProps) => BaseField;
553
- /**
554
- * File upload field.
555
- */
556
- file: (name: string, label: string, props?: FieldProps) => BaseField;
557
- /**
558
- * Hidden field (no UI).
559
- */
560
- hidden: (name: string, props?: FieldProps) => BaseField;
793
+ /** Text input field. */text: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Email input field with default placeholder. */
794
+ email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
795
+ url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
796
+ tel: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Password input field. */
797
+ password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
798
+ /** Number input field. No implicit `min` — pass `{ min }` to add one, so the
799
+ * builder never injects validation the author didn't write (signed
800
+ * quantities like deltas / temperatures stay valid). */
801
+ number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
802
+ textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
803
+ select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
804
+ combobox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Multi-select field. */
805
+ 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. */
806
+ dependentSelect: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Switch/toggle field. */
807
+ switch: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Boolean field (alias for switch). */
808
+ boolean: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Checkbox field. */
809
+ checkbox: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Radio button group field. */
810
+ radio: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>; /** Date picker field. */
811
+ date: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Tag input field. */
812
+ tags: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Slug field. */
813
+ slug: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** File upload field. */
814
+ file: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Hidden field (no UI). */
815
+ hidden: <T extends FieldValues = FieldValues>(name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
561
816
  /**
562
817
  * Group field for nested objects.
563
- * Renders itemFields as a sub-grid within the form.
818
+ * Renders itemFields as a sub-grid. Child names are relative (e.g. "street"),
819
+ * FormGenerator prefixes them with the group name at render time.
564
820
  *
565
821
  * @example
566
822
  * ```ts
567
823
  * field.group("address", "Address", [
568
824
  * field.text("street", "Street"),
569
825
  * field.text("city", "City"),
570
- * field.text("zip", "ZIP Code"),
571
- * ], { cols: 3 })
826
+ * ], { cols: 2 })
572
827
  * ```
573
828
  */
574
- group: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
829
+ group: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
575
830
  /**
576
- * Array/repeatable field.
577
- * Renders a dynamic list of sub-forms using react-hook-form's useFieldArray.
831
+ * Array/repeatable field backed by react-hook-form's useFieldArray.
578
832
  *
579
833
  * @example
580
834
  * ```ts
@@ -584,41 +838,108 @@ declare const field: {
584
838
  * ])
585
839
  * ```
586
840
  */
587
- array: (name: string, label: string, itemFields: BaseField[], props?: FieldProps) => BaseField;
841
+ array: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
588
842
  /**
589
843
  * Custom field with a render function.
590
- * Bypasses the component registry entirely.
844
+ * Bypasses the component registry — full control over rendering.
845
+ *
846
+ * The render callback receives the complete `FieldComponentProps` including
847
+ * `fieldId`, `errorId`, `shouldShowError`, `error`, `rules`, and `control`.
848
+ *
849
+ * Use `shouldShowError` (not `!!error`) to drive `aria-invalid` and error
850
+ * visibility so timing mirrors the CSS `:user-invalid` pseudo-class.
591
851
  *
592
852
  * @example
593
- * ```ts
594
- * field.custom("skills", "Skills", ({ control, disabled }) => (
595
- * <SkillSelector control={control} disabled={disabled} />
853
+ * ```tsx
854
+ * field.custom("skills", "Skills", ({ control, shouldShowError, errorId, error, fieldId }) => (
855
+ * <div>
856
+ * <SkillSelector
857
+ * id={fieldId}
858
+ * control={control}
859
+ * aria-invalid={shouldShowError || undefined}
860
+ * aria-errormessage={shouldShowError ? errorId : undefined}
861
+ * />
862
+ * {shouldShowError && (
863
+ * <p id={errorId} role="alert" className="text-sm text-destructive">
864
+ * {error?.message}
865
+ * </p>
866
+ * )}
867
+ * </div>
596
868
  * ))
597
869
  * ```
598
870
  */
599
- custom: (name: string, label: string, render: CustomRenderFn, props?: FieldProps) => BaseField;
871
+ custom: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, props?: FieldProps<T>) => BaseField<T>;
872
+ /**
873
+ * Returns a typed field builder with `TFieldValues` fixed.
874
+ * Every field name is validated against `Path<TFieldValues>` at the call site —
875
+ * no need to repeat the generic on each individual builder call.
876
+ *
877
+ * @example
878
+ * ```ts
879
+ * interface ContactForm {
880
+ * firstName: string;
881
+ * email: string;
882
+ * address: { street: string; city: string };
883
+ * }
884
+ *
885
+ * const f = field.for<ContactForm>()
886
+ *
887
+ * const schema = defineSchema<ContactForm>({
888
+ * sections: [{
889
+ * fields: [
890
+ * f.text("firstName", "First Name"), // ✓
891
+ * f.email("email", "Email"), // ✓
892
+ * f.text("typo", "Label"), // ✗ TypeScript error
893
+ * ],
894
+ * }],
895
+ * })
896
+ * ```
897
+ */
898
+ for: <T extends FieldValues>() => {
899
+ text: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
900
+ email: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
901
+ url: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
902
+ tel: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
903
+ password: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
904
+ number: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
905
+ textarea: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
906
+ select: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
907
+ combobox: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
908
+ multiselect: (name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>;
909
+ dependentSelect: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
910
+ switch: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
911
+ boolean: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
912
+ checkbox: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
913
+ radio: (name: Path<T>, label: string, options: FieldOption[], props?: FieldProps<T>) => BaseField<T>;
914
+ date: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
915
+ tags: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
916
+ slug: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
917
+ file: (name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
918
+ hidden: (name: Path<T>, props?: FieldProps<T>) => BaseField<T>;
919
+ group: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
920
+ array: (name: Path<T>, label: string, itemFields: BaseField[], props?: FieldProps<T>) => BaseField<T>;
921
+ custom: (name: Path<T>, label: string, render: (props: FieldComponentProps<T>) => ReactNode, builderProps?: FieldProps<T>) => BaseField<T>;
922
+ };
600
923
  };
601
924
  /**
602
925
  * Create a section definition with sensible defaults.
603
926
  *
604
- * @param id - Unique section identifier
605
- * @param title - Section title
606
- * @param fields - Array of field definitions
607
- * @param props - Additional section configuration
608
- *
609
927
  * @example
610
928
  * ```ts
611
929
  * section("personal", "Personal Info", [
612
930
  * field.text("name", "Name", { required: true }),
613
931
  * field.email("email", "Email"),
614
- * ], { cols: 2, variant: "card" })
932
+ * ], { cols: 2 })
615
933
  * ```
616
934
  */
617
935
  declare function section<TFieldValues extends FieldValues = FieldValues>(id: string, title: string, fields: BaseField<TFieldValues>[], props?: SectionProps<TFieldValues>): Section<TFieldValues>;
618
936
  /**
619
937
  * Create a section without a title (transparent section).
620
938
  * Useful for grouping fields without visual separation.
939
+ *
940
+ * Accepts `BaseField[]` (no generic) so mixed-type field arrays don't trigger
941
+ * conflicting type inference across different field name generics.
621
942
  */
622
- declare function sectionUntitled<TFieldValues extends FieldValues = FieldValues>(fields: BaseField<TFieldValues>[], props?: Omit<SectionProps<TFieldValues>, "variant">): Section<TFieldValues>;
943
+ declare function sectionUntitled(fields: BaseField[], props?: Omit<SectionProps, "variant">): Section;
623
944
  //#endregion
624
- 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 };
945
+ 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 SchemaIssue, type Section, type SectionLayoutProps, type SectionRenderProps, type ValidationRuleObject, type Variant, buildFieldDefaults, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractWatchNames, field, flattenSchema, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeDefaultValues, mergeSchemas, omitFields, pickFields, section, sectionUntitled, validateSchema };