@classytic/formkit 1.4.0 → 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/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.0] - 2026-07-02
9
+
10
+ Stability / hardening release. Two intentional **behavioral changes** are called
11
+ out below — read them before upgrading.
12
+
13
+ ### Behavioral changes
14
+
15
+ - **`field.number()` no longer injects `min: 0`.** The builder previously added
16
+ an implicit non-negative constraint; forms relying on it must now pass
17
+ `{ min: 0 }` explicitly. Signed quantities (deltas, temperatures) are valid by
18
+ default.
19
+ - **Missing components render a visible placeholder in production** (previously
20
+ a silent null). The wrong-widget `text` fallback for unregistered types was
21
+ removed entirely. Use the new `onMissingComponent` provider callback for
22
+ telemetry. Exception: `hidden` fields with no registered component render
23
+ nothing by design.
24
+
25
+ ### Added
26
+
27
+ - `validateSchema(schema)` — structural schema linting (missing name/type,
28
+ duplicate names, `itemFields` on non-containers, unknown DSL operators),
29
+ exported from both entries and auto-run in dev by `FormGenerator`.
30
+ - `focusFirstError(root?)` — focuses the first visibly-invalid field in layout
31
+ order; retries across two animation frames so it works from
32
+ `handleSubmit(onValid, () => focusFirstError())` on the very first submit.
33
+ - `loadOptions` now receives `{ signal: AbortSignal }` as a second argument;
34
+ superseded/unmounted loads are aborted. New opt-in `cacheOptions` memoizes
35
+ results per watched-value signature (bounded).
36
+ - `onMissingComponent` callback on `FormSystemProvider`.
37
+ - `colSpan` field prop (surfaced as `data-col-span`; `fullWidth` now also emits
38
+ an inline `grid-column: 1 / -1` so full-width layout works without scanning
39
+ the package with Tailwind).
40
+ - `resetOnSchemaChange` option on `useFormKit` for DB-loaded schema swaps.
41
+ - Dev warning when `loadOptions` is used without `watchNames` (whole-form
42
+ refetch storms).
43
+ - `buildFieldDefaults` exported; array-item seeds are now type-aware (`false`
44
+ for checkbox/switch, `[]` for multiselect/tags, `undefined` for number).
45
+ - `mergeDefaultValues(base, override)` exported (both entries) — the exact
46
+ Date-safe deep merge `useFormKit` applies between schema defaults and caller
47
+ `defaultValues`, for wrappers that re-seed via `form.reset(...)`.
48
+
49
+ ### Fixed
50
+
51
+ - **Rules-of-hooks crash**: `useFieldComponent` and the field-props memo are
52
+ hoisted above the group/array fallback returns, so a registry that changes
53
+ after mount (code-split adapters) can no longer change the hook count.
54
+ `eslint-plugin-react-hooks` is now enforced in CI-lint.
55
+ - **Date / class-instance defaults survive merging**: `useFormKit`'s deep-merge
56
+ now replaces non-plain objects (Date, File, class instances) wholesale
57
+ instead of spread-merging them into `{}`.
58
+ - Function conditions with `watchNames` receive properly **nested** watched
59
+ values (`(v) => v.address.city` works; previously flat `"address.city"` keys
60
+ made them throw and hide the field).
61
+ - `extractDefaultValues` recurses into nested groups (deep defaults were
62
+ dropped beyond one level); shares one walker with the async variant.
63
+ - Fallback UI (array items, error boundary, missing-component placeholder) uses
64
+ structural inline styles + `currentColor` instead of Tailwind classes that
65
+ don't exist in consumer builds; `formkit-*` classes remain as theming hooks.
66
+ - `process.env` reads are guarded (`isDev()` helper), so bundler-less ESM
67
+ environments no longer throw `ReferenceError`.
68
+
8
69
  ## [1.4.0] - 2026-05-26
9
70
 
10
71
  ### Added
package/README.md CHANGED
@@ -8,8 +8,12 @@ Headless, type-safe form generation engine for React 19. Schema-driven with full
8
8
 
9
9
  ## Features
10
10
 
11
- - **Minimal boilerplate** - `useFormKit` hook: 5 lines to set up a complete form
12
- - **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
11
+ - **Headless** - Bring your own UI components (Shadcn, MUI, Chakra, etc.) via a
12
+ one-time component/layout adapter. Already using shadcn? Skip the adapter
13
+ entirely with the prebuilt one in **[`@classytic/fluid/formkit`](https://www.npmjs.com/package/@classytic/fluid)** —
14
+ its `SchemaForm` is genuinely a schema + `onSubmit` away.
15
+ - **Minimal boilerplate** - once an adapter is registered, `useFormKit` wires a
16
+ full form (defaults + RHF + generator props) in a few lines
13
17
  - **Schema-driven** - Define forms with JSON/TypeScript schemas, defaults extracted automatically
14
18
  - **Type-safe** - Full TypeScript support with generics
15
19
  - **React Hook Form** - Built on top of the best form library, referentially stable return values
@@ -20,7 +24,7 @@ Headless, type-safe form generation engine for React 19. Schema-driven with full
20
24
  - **Responsive layouts** - Multi-column grid layouts
21
25
  - **Accessibility** - Auto-generated `fieldId`, `error`, and `fieldState` props
22
26
  - **Validation helpers** - `buildValidationRules` generates RHF rules from schema props
23
- - **Lightweight** - ~12KB gzipped (peer deps excluded), tree-shakeable
27
+ - **Lightweight** - ~17KB gzipped (peer deps excluded, everything included), tree-shakeable
24
28
 
25
29
  ## Requirements
26
30
 
@@ -127,9 +131,16 @@ const layouts: LayoutRegistry = {
127
131
  {children}
128
132
  </div>
129
133
  ),
130
- grid: ({ children, cols = 1 }) => (
131
- <div className={`grid grid-cols-${cols} gap-4`}>{children}</div>
132
- ),
134
+ grid: ({ children, cols = 1 }) => {
135
+ // Map to STATIC class names. A template like `grid-cols-${cols}` is never
136
+ // generated by Tailwind's JIT (it only sees whole class strings in source).
137
+ const COLS: Record<number, string> = {
138
+ 1: "grid-cols-1",
139
+ 2: "grid-cols-1 md:grid-cols-2",
140
+ 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
141
+ };
142
+ return <div className={`grid gap-4 ${COLS[cols] ?? "grid-cols-1"}`}>{children}</div>;
143
+ },
133
144
  };
134
145
 
135
146
  export function FormProvider({ children }: { children: React.ReactNode }) {
@@ -198,6 +209,26 @@ export default function SignupPage() {
198
209
  }
199
210
  ```
200
211
 
212
+ ## Type safety
213
+
214
+ Field **names** are enforced against your form type at *authoring* time by the
215
+ builders. Use `field.for<T>()`, `defineField<T>()`, or `section<T>()` and a typo
216
+ is a compile error:
217
+
218
+ ```ts
219
+ const f = field.for<SignupData>();
220
+ f.text("emial", "Email"); // ✗ compile error — "emial" is not a key of SignupData
221
+ f.text("email", "Email"); // ✓
222
+ ```
223
+
224
+ The `<FormGenerator schema>` / `useFormKit({ schema })` props themselves accept
225
+ `FormSchema<any>` **by design**: a schema's condition functions (`(values) => …`)
226
+ make `FormSchema<T>` contravariant in `T`, so pinning the prop to the form's `T`
227
+ would force a cast at every call site. Author with the typed builders for name
228
+ safety; the generator stays permissive so any built schema drops in without
229
+ ceremony. `itemFields` (group/array children) use relative names and are
230
+ resolved at render time, so they are typed loosely on purpose.
231
+
201
232
  ## API Reference
202
233
 
203
234
  ### useFormKit
@@ -216,6 +247,7 @@ const form = useFormKit({
216
247
  disabled: false, // optional
217
248
  variant: "compact", // optional
218
249
  className: "my-form", // optional
250
+ resetOnSchemaChange: true, // optional — re-seed on schema swap
219
251
  mode: "onBlur", // any useForm option
220
252
  });
221
253
 
@@ -234,7 +266,9 @@ return (
234
266
  );
235
267
  ```
236
268
 
237
- Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority).
269
+ Schema `defaultValue` fields are automatically extracted and merged with any explicit `defaultValues` you provide (explicit values take priority). Nested/namespaced defaults are extracted as a nested object (`{ address: { city } }`), so react-hook-form's dot-path resolution applies them correctly.
270
+
271
+ Pass `resetOnSchemaChange: true` to re-seed the form when the `schema` identity changes at runtime (e.g. a schema loaded from a DB is swapped in). It's off by default because a reset discards in-progress edits.
238
272
 
239
273
  `generatorProps` is memoized — it only recomputes when `schema`, `control`, `disabled`, `variant`, or `className` change.
240
274
 
@@ -361,9 +395,14 @@ interface FieldComponentProps<T extends FieldValues = FieldValues>
361
395
  isDirty: boolean;
362
396
  isTouched: boolean;
363
397
  isValidating: boolean;
398
+ isSubmitted: boolean;
364
399
  error?: FieldError;
365
400
  };
366
401
  fieldId: string; // Generated ID for label-input association (e.g. "formkit-field-email")
402
+ errorId: string; // ID for the error node — set as aria-errormessage on the input
403
+ shouldShowError: boolean; // true only after touch/submit (mirrors :user-invalid timing)
404
+ rules: RegisterOptions; // Pre-computed RHF rules from schema (required/min/max/pattern…)
405
+ isLoading?: boolean; // true while async loadOptions is in flight
367
406
  }
368
407
  ```
369
408
 
@@ -466,6 +505,64 @@ function FormInput({ field, control, error, fieldId }: FieldComponentProps) {
466
505
 
467
506
  Maps `required`, `min`, `max`, `minLength`, `maxLength`, and `pattern` from the field schema to RHF-compatible rules with auto-generated error messages.
468
507
 
508
+ ## Validation & helpers
509
+
510
+ ### validateSchema
511
+
512
+ Structurally validate a schema (great for DB-loaded / `resetOnSchemaChange`
513
+ schemas). Server-safe; returns `SchemaIssue[]` (empty ⇒ OK). `FormGenerator`
514
+ also runs it automatically in dev and `console.warn`s any issues.
515
+
516
+ ```ts
517
+ import { validateSchema } from "@classytic/formkit"; // or /server
518
+
519
+ for (const issue of validateSchema(schema)) {
520
+ console.warn(`${issue.severity} ${issue.path}: ${issue.message}`);
521
+ }
522
+ // Detects: missing name/type, duplicate names (namespace-aware),
523
+ // itemFields on a non-container, empty containers, unknown DSL operators.
524
+ ```
525
+
526
+ It validates **shape**, not your component registry — an unregistered field
527
+ `type` is a rendering concern, surfaced separately (see `onMissingComponent`).
528
+
529
+ ### focusFirstError
530
+
531
+ Focus + scroll to the first visibly-invalid field, in layout order. Wire it into
532
+ the submit's invalid handler:
533
+
534
+ ```tsx
535
+ import { focusFirstError } from "@classytic/formkit";
536
+
537
+ <form onSubmit={handleSubmit(onValid, () => focusFirstError())}>
538
+ ```
539
+
540
+ React Hook Form calls the invalid handler *before* React commits the re-render
541
+ that marks fields invalid, so if the immediate query finds nothing,
542
+ `focusFirstError` automatically retries on the next two animation frames — the
543
+ plain wiring above works on the very first submit. It returns `true` when a
544
+ field was focused immediately, `false` when the attempt was deferred.
545
+
546
+ ### onMissingComponent
547
+
548
+ If a field's `type` has no registered component, the form renders a **visible
549
+ placeholder** (dev and prod — never a silent drop or a wrong-widget text input)
550
+ and notifies this callback so you can log the gap. `hidden` fields are the one
551
+ exception: with no registered component they render nothing by design (the
552
+ value still lives in form state); register a `hidden` component to override.
553
+
554
+ ```tsx
555
+ <FormSystemProvider
556
+ components={components}
557
+ layouts={layouts}
558
+ onMissingComponent={(type) => reportToTelemetry("formkit_missing_component", { type })}
559
+ >
560
+ ```
561
+
562
+ > **Security note:** `buildValidationRules` compiles a field's `pattern` into a
563
+ > `new RegExp(...)`. If your schemas are ever authored by untrusted users, treat
564
+ > `pattern` as a ReDoS surface — validate/limit it before persisting.
565
+
469
566
  ## Server Components
470
567
 
471
568
  The `@classytic/formkit/server` entry point exports server-safe utilities with no React hooks or client-side code:
@@ -494,6 +591,24 @@ import type {
494
591
 
495
592
  Use this entry point in React Server Components to define schemas, evaluate conditions, or use `cn` without pulling in client-side code.
496
593
 
594
+ ### Passing a schema across the RSC → Client boundary
595
+
596
+ `FormGenerator` is a Client Component (it drives react-hook-form). If you build
597
+ a schema in a Server Component and pass it as a prop into the client, React
598
+ requires that prop to be **serializable** — so **function** members won't cross
599
+ the boundary:
600
+
601
+ - ❌ function `condition: (values) => …`, custom `render`, and `loadOptions`
602
+ (functions aren't serializable)
603
+ - ✅ **DSL** conditions (`{ watch, operator, value }`), and all the plain data
604
+ fields (`name`, `type`, `label`, `options`, `required`, `min`/`max`, …)
605
+
606
+ For fully static, server-defined schemas use DSL conditions. If a schema needs
607
+ function conditions / custom renders / async options, define it **inside the
608
+ Client Component** (e.g. the file that renders `FormGenerator`) rather than
609
+ passing it down from an RSC. This is a general Next.js constraint, not specific
610
+ to formkit.
611
+
497
612
  ## Advanced Features
498
613
 
499
614
  ### Conditional Fields (Function)
@@ -507,6 +622,12 @@ Use this entry point in React Server Components to define schemas, evaluate cond
507
622
  }
508
623
  ```
509
624
 
625
+ > **Performance:** a function condition can't be statically analyzed, so the
626
+ > field subscribes to the **whole form** and re-evaluates on every change. For
627
+ > large forms prefer a DSL rule (below, whose watched paths are auto-scoped), or
628
+ > add `watchNames` to scope the subscription yourself:
629
+ > `{ condition: (v) => v.accountType === "business", watchNames: ["accountType"] }`.
630
+
510
631
  ### Conditional Fields (DSL Rules)
511
632
 
512
633
  ```ts
@@ -604,14 +725,20 @@ const components = {
604
725
  name: "city",
605
726
  type: "select",
606
727
  watchNames: ["country"],
607
- loadOptions: async (values) => {
608
- const cities = await fetchCities(values.country);
728
+ loadOptions: async (values, { signal }) => {
729
+ // Forward `signal` so a superseded/unmounted load is cancelled.
730
+ const cities = await fetchCities(values.country, { signal });
609
731
  return cities.map(c => ({ label: c.name, value: c.id }));
610
732
  },
611
733
  debounceMs: 300,
734
+ cacheOptions: true, // reuse the last fetch when a dependency is re-selected
612
735
  }
613
736
  ```
614
737
 
738
+ > `cacheOptions` (default `false`) memoizes results per watched-value signature
739
+ > for the field's lifetime, so toggling `country` back and forth doesn't re-hit
740
+ > the API. Leave it off when the options can change server-side between loads.
741
+
615
742
  ### Custom Section Render
616
743
 
617
744
  ```ts
package/dist/index.d.mts CHANGED
@@ -138,6 +138,13 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
138
138
  variant?: Variant;
139
139
  /** Whether field should span full width in grid */
140
140
  fullWidth?: boolean;
141
+ /**
142
+ * Span N columns of the section grid (e.g. `2` of a 3-col section). `1` (or
143
+ * unset) is the default single cell; `fullWidth` still means "span the whole
144
+ * row" and takes precedence. Applied as an inline `grid-column: span N` so it
145
+ * works regardless of the host's Tailwind content scan.
146
+ */
147
+ colSpan?: number;
141
148
  /** Custom CSS class name */
142
149
  className?: string;
143
150
  /**
@@ -199,13 +206,33 @@ interface BaseField<TFieldValues extends FieldValues = FieldValues> {
199
206
  /**
200
207
  * Dynamic options loaded based on current form values.
201
208
  * Useful for dependent selects (e.g., state depends on country).
209
+ *
210
+ * Receives an `AbortSignal` in the second arg — forward it to `fetch` (or
211
+ * abort your own request on it) so a superseded / unmounted load is cancelled
212
+ * instead of racing to completion:
213
+ *
214
+ * ```ts
215
+ * loadOptions: (values, { signal }) =>
216
+ * fetch(`/api/cities?country=${values.country}`, { signal }).then(r => r.json())
217
+ * ```
202
218
  */
203
- loadOptions?: (formValues: Partial<TFieldValues>) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
219
+ loadOptions?: (formValues: Partial<TFieldValues>, options?: {
220
+ signal: AbortSignal;
221
+ }) => Promise<(FieldOption | FieldOptionGroup)[]> | (FieldOption | FieldOptionGroup)[];
204
222
  /**
205
223
  * Error callback for loadOptions failures.
206
224
  * Called when loadOptions rejects. Defaults to console.error.
207
225
  */
208
226
  onLoadError?: (error: unknown) => void;
227
+ /**
228
+ * Memoize async `loadOptions` results per set of watched values, so toggling
229
+ * a dependency back and forth (e.g. re-selecting a country) reuses the last
230
+ * fetch instead of hitting the API again. Off by default — enable only when
231
+ * the options are stable for a given input, since the cache lives for the
232
+ * field's lifetime and won't see server-side changes. Bounded (LRU-ish) so it
233
+ * can't grow without limit.
234
+ */
235
+ cacheOptions?: boolean;
209
236
  /**
210
237
  * Sub-fields for `group` and `array` field types.
211
238
  *
@@ -553,6 +580,8 @@ interface FormSystemContextValue {
553
580
  components: ComponentRegistry;
554
581
  /** Registered layout components */
555
582
  layouts: LayoutRegistry;
583
+ /** Notified when a field type has no registered component (see provider). */
584
+ onMissingComponent?: (type: string, variant?: string) => void;
556
585
  }
557
586
  /**
558
587
  * Form system provider props.
@@ -562,6 +591,13 @@ interface FormSystemProviderProps {
562
591
  components?: ComponentRegistry;
563
592
  /** Layout component registry */
564
593
  layouts?: LayoutRegistry;
594
+ /**
595
+ * Called (once per type) when a field's `type` has no registered component
596
+ * and the visible placeholder is shown instead. Use it to log/report the
597
+ * missing registration to your telemetry — the form still renders a
598
+ * placeholder rather than silently dropping the field.
599
+ */
600
+ onMissingComponent?: (type: string, variant?: string) => void;
565
601
  /** Children content */
566
602
  children: ReactNode;
567
603
  }
@@ -718,6 +754,7 @@ declare const FieldWrapper: typeof FieldWrapperImpl;
718
754
  declare function FormSystemProvider({
719
755
  components,
720
756
  layouts,
757
+ onMissingComponent,
721
758
  children
722
759
  }: FormSystemProviderProps): FormElement;
723
760
  /**
@@ -739,11 +776,16 @@ declare function useFormSystem(): FormSystemContextValue;
739
776
  * 1. Variant-specific component: `components[variant][type]`
740
777
  * 2. Type-specific component: `components[type]`
741
778
  * 3. Default component: `components["default"]`
742
- * 4. Text fallback: `components["text"]`
779
+ *
780
+ * There is intentionally NO "text" fallback: rendering a text input for an
781
+ * unregistered `date`/`switch`/etc. is a silent wrong-widget bug. A missing
782
+ * registration instead renders a visible placeholder (`MissingFieldComponent`)
783
+ * — in production too — and notifies `onMissingComponent`, so the gap is
784
+ * observable rather than a mystery empty box.
743
785
  *
744
786
  * @param type - Field type identifier
745
787
  * @param variant - Optional variant name
746
- * @returns Field component or fallback
788
+ * @returns Field component or the visible placeholder
747
789
  *
748
790
  * @internal
749
791
  */
@@ -837,6 +879,54 @@ declare function defineSection<TFieldValues extends FieldValues = FieldValues>(s
837
879
  * ```
838
880
  */
839
881
  declare function extractDefaultValues<TFieldValues extends FieldValues = FieldValues>(schema: FormSchema<TFieldValues>): Partial<TFieldValues>;
882
+ interface SchemaIssue {
883
+ /** Where the issue is, e.g. `sections[0].fields[2]` or `sections[1]`. */
884
+ path: string;
885
+ code: "missing-name" | "missing-type" | "duplicate-name" | "itemfields-on-noncontainer" | "empty-container" | "unknown-operator";
886
+ /** `error` = will misbehave at runtime; `warning` = suspicious but tolerated. */
887
+ severity: "error" | "warning";
888
+ message: string;
889
+ }
890
+ /**
891
+ * Structurally validate a form schema and return a list of issues (empty ⇒ OK).
892
+ * Server-safe (no hooks/DOM), so you can run it when a schema is loaded from a
893
+ * DB, in a test, or in a dev boot check. It validates SHAPE, not your component
894
+ * registry — an unknown field `type` is a registry concern, not a schema error.
895
+ *
896
+ * Checks: missing `name`/`type`, duplicate names (namespace-aware), `itemFields`
897
+ * on a non-container type, containers with no `itemFields`, and unknown DSL
898
+ * condition operators.
899
+ */
900
+ declare function validateSchema(schema: FormSchema): SchemaIssue[];
901
+ /**
902
+ * Build a default-value object for a flat list of fields — used to seed a new
903
+ * array item (or a group) from its `itemFields`.
904
+ *
905
+ * Recurses into nested `group` children (→ nested object) and seeds nested
906
+ * `array` children as `[]`, so appending an item never leaves a deep sub-field
907
+ * `undefined`. That matters because a missing deep field can trip a resolver
908
+ * (zod et al.) into a spurious "required" error the moment the row is added.
909
+ * Leaf fields without an explicit `defaultValue` seed to `""` (a controlled
910
+ * empty value RHF is happy with).
911
+ */
912
+ declare function buildFieldDefaults(fields: BaseField[] | undefined): Record<string, unknown>;
913
+ /**
914
+ * Deep-merge `override` onto `base` with default-values semantics: nested
915
+ * plain objects merge recursively; arrays, primitives, and class instances
916
+ * (Date, File, …) from `override` replace wholesale.
917
+ *
918
+ * This is the merge `useFormKit` applies between schema-extracted defaults and
919
+ * caller-provided `defaultValues`. Exported so wrappers that re-seed a form at
920
+ * runtime (e.g. an edit sheet swapping entities) can reproduce the exact same
921
+ * merge for `form.reset(...)`:
922
+ *
923
+ * @example
924
+ * ```ts
925
+ * const merged = mergeDefaultValues(extractDefaultValues(schema), entity);
926
+ * form.reset(merged);
927
+ * ```
928
+ */
929
+ declare function mergeDefaultValues(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown>;
840
930
  /**
841
931
  * Async version of `extractDefaultValues` for large schemas (50+ fields).
842
932
  * Yields to the main thread after each section so that the browser can
@@ -1005,7 +1095,10 @@ declare const field: {
1005
1095
  email: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** URL input field with default placeholder. */
1006
1096
  url: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Phone/tel input field with default placeholder. */
1007
1097
  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). */
1098
+ password: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>;
1099
+ /** Number input field. No implicit `min` — pass `{ min }` to add one, so the
1100
+ * builder never injects validation the author didn't write (signed
1101
+ * quantities like deltas / temperatures stay valid). */
1009
1102
  number: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Textarea field with default 3 rows. */
1010
1103
  textarea: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, props?: FieldProps<T>) => BaseField<T>; /** Select dropdown field. */
1011
1104
  select: <T extends FieldValues = FieldValues>(name: Path<T>, label: string, options: (FieldOption | FieldOptionGroup)[], props?: FieldProps<T>) => BaseField<T>; /** Searchable combobox field. */
@@ -1166,6 +1259,14 @@ interface UseFormKitOptions<TFieldValues extends FieldValues = FieldValues> exte
1166
1259
  variant?: Variant;
1167
1260
  /** Root element className */
1168
1261
  className?: string;
1262
+ /**
1263
+ * Re-seed the form with the schema's freshly-extracted defaults whenever the
1264
+ * `schema` identity changes (e.g. a schema fetched from a DB is swapped in).
1265
+ * Off by default because a reset discards in-progress edits — enable it only
1266
+ * when a schema swap should intentionally re-initialise the form. Skipped when
1267
+ * `defaultValues` is an async factory (the caller owns that reset).
1268
+ */
1269
+ resetOnSchemaChange?: boolean;
1169
1270
  }
1170
1271
  /**
1171
1272
  * Return value from useFormKit.
@@ -1196,4 +1297,30 @@ interface UseFormKitReturn<TFieldValues extends FieldValues = FieldValues> exten
1196
1297
  */
1197
1298
  declare function useFormKit<TFieldValues extends FieldValues = FieldValues>(options: UseFormKitOptions<TFieldValues>): UseFormKitReturn<TFieldValues>;
1198
1299
  //#endregion
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 };
1300
+ //#region src/focus.d.ts
1301
+ /**
1302
+ * Focus (and scroll to) the first field currently shown as invalid.
1303
+ *
1304
+ * It queries the rendered `[data-formkit-field][data-invalid]` wrapper — which
1305
+ * `FormGenerator` marks once errors are visible (after touch / submit) — so it
1306
+ * targets the first error in **visual order**, not react-hook-form's error-key
1307
+ * order (which isn't guaranteed to match the layout). Wire it into your submit's
1308
+ * invalid handler:
1309
+ *
1310
+ * ```ts
1311
+ * form.handleSubmit(onValid, () => focusFirstError());
1312
+ * ```
1313
+ *
1314
+ * **Timing:** react-hook-form calls the invalid handler *before* React commits
1315
+ * the re-render that sets `data-invalid` on the wrappers, so on the very first
1316
+ * invalid submit a synchronous query finds nothing. When the immediate attempt
1317
+ * misses, this retries on the next two animation frames (the commit lands
1318
+ * before the next paint), so the plain wiring above Just Works.
1319
+ *
1320
+ * @param root Optional container to search within (defaults to `document`).
1321
+ * @returns `true` if a field was focused immediately; `false` if not found yet
1322
+ * (a deferred retry may still focus it on the next frame).
1323
+ */
1324
+ declare function focusFirstError(root?: HTMLElement | Document | null): boolean;
1325
+ //#endregion
1326
+ 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 SchemaIssue, type Section, type SectionLayoutProps, type SectionRenderProps, SectionRenderer, type SectionRendererProps, type UseFormKitOptions, type UseFormKitReturn, type ValidationRuleObject, type ValidationRules, type Variant, applyServerErrors, buildFieldDefaults, buildValidationRules, cn, defineField, defineSchema, defineSection, evaluateCondition, extendSection, extractDefaultValues, extractDefaultValuesAsync, extractWatchNames, field, flattenSchema, focusFirstError, isArrayField, isChoiceField, isConditionalField, isContainerField, isDateField, isDynamicField, isNumericField, isTextField, mergeDefaultValues, mergeSchemas, omitFields, pickFields, section, sectionUntitled, shallowEqual, useFieldComponent, useFormKit, useFormSystem, useLayoutComponent, validateSchema };