formstand 0.15.2 → 0.17.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
@@ -1,5 +1,292 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 — 2026-09-17
4
+
5
+ ### Added
6
+
7
+ - **`form.addErrors(errors)`.** The merge-many sibling of `setError`:
8
+ takes a server response's own runtime-keyed error map as-is and merges
9
+ it into the server channel — no per-key `FieldPath` cast, no
10
+ `setErrors` wholesale replacement. An empty array removes its key,
11
+ like `setError`. The docs' own examples now use it.
12
+ - **`SubmitOptions.onError` and a dev-mode swallowed-failure warning.**
13
+ A throwing submit handler resolves `{kind: "error"}` writing no state
14
+ anywhere, so the fire-and-forget `handleSubmit` form could swallow a
15
+ save failure completely — button re-enables, zero feedback. `onError`
16
+ is the save-failure hook mirroring `onInvalid`, and dev builds warn
17
+ when an unobserved handler failure resolves without one.
18
+ - **`useFieldArray` parity with its siblings.** The return gains `path`,
19
+ `firstError`, and the array-level server channel (`setError` /
20
+ `clearError`) — a "too many rows" verdict from the API is set and
21
+ cleared from the hook that owns the array. Custom `FieldArrayFormApi`
22
+ implementations opt in via the optional `setError`/`clearErrors`
23
+ members (same contract as the optional `validateField`).
24
+ - **`useIsValidating(form, path?)`.** The boolean-only in-flight flag,
25
+ form-level (whole-form pass or any field) or scoped to a subtree —
26
+ previously reachable only through a selector, asymmetric with
27
+ `useIsSubmitting`.
28
+ - **`usePersistForm(form, options)`.** The React lifecycle wrapper
29
+ `persistForm` was missing: owns the mount/dispose effect (StrictMode
30
+ safe), returns a reference-stable handle safe to close over in submit
31
+ handlers, and reads options when the subscription starts (the
32
+ `useForm` locked-options rule).
33
+ - **`createFormHooks` binds the newer hooks.** `use{Name}Values`,
34
+ `use{Name}Fields` (position-typed like the plain `useFields`), and
35
+ `use{Name}IsValidating` join the factory.
36
+
37
+ ### Fixed
38
+
39
+ - **`numberInputProps` keeps the value on a partial entry.** The
40
+ `type="number"` builder wrote `emptyValue` for BOTH empty and invalid
41
+ text, so a transient "-" or "1e" nulled the field mid-keystroke
42
+ (error flash, dirty flip) — diverging from `useNumberInput`'s
43
+ deliberate keep-the-value semantics. Invalid now keeps the stored
44
+ value; empty still clears. (Browsers report `""` for badInput on
45
+ number inputs, so `useNumberInput` remains the binding that preserves
46
+ every keystroke.)
47
+ - **Tuples resolve positionally in the typed paths.** Tuples fell into
48
+ the generic-array arms of `FieldPath` and `UnionValueAt`, which union
49
+ every element — so nested paths under heterogeneous tuples were
50
+ rejected (`"hetero.0.a"` unbindable), out-of-range indices
51
+ (`"coord.5"`) accepted with a lying element type, and a discriminated
52
+ union below a tuple position unbindable through `useVariantField`.
53
+ Both types gained a tuple arm keyed on the literal indices; `"coord.0"`
54
+ spellings the CLI generates are unchanged.
55
+ - **Platform containers are path leaves.** `File`/`Blob` (matched
56
+ structurally, so no DOM lib requirement), `Map`, `Set`, `WeakMap`,
57
+ `WeakSet`, `Promise`, and functions no longer descend as records:
58
+ bogus sub-paths like `"file.name"` or `"meta.size"` used to
59
+ type-check, and a `setValue` through one would have spread the
60
+ instance into a plain object.
61
+ - **Literal dotted keys are no longer offered as paths.** A key named
62
+ `"a.b"` appeared in `FieldPath` but resolved to `never` (the runtime
63
+ walk splits on `"."`), so the offer was the lie; it is now excluded
64
+ where it is offered rather than where it resolves.
65
+ - **`validateField` no longer crashes on prototype-key paths.** The
66
+ schema walk read the ZodObject shape with a bare index, so a
67
+ runtime-built path like `"user.constructor.x"` (server error fields
68
+ mapped onto `validateField`) resolved Object's constructor as a
69
+ "schema" and threw — and the subschema cache kept the poisoned entry.
70
+ Both walks now do own-key reads, like every other walk in the library.
71
+ - **Persist discards version-downgrade drafts instead of adopting the
72
+ wrapper.** A draft written with `version: n` is a `{__v, values}`
73
+ wrapper; if the app later shipped without the `version` option, the
74
+ wrapper's keys evaded the shape guard and the wrapper OBJECT became
75
+ the form's values, then re-persisted. A versioned draft under a
76
+ version-less config is now discarded like any other draft the config
77
+ cannot vouch for.
78
+ - **Debounced validation no longer survives `reset()`.** A pending
79
+ debounce timer fired after `reset()`/`adoptValues()` and re-committed
80
+ errors onto the pristine form. The fire now bails when the form is
81
+ pristine (values is the initialValues reference) or when the path's
82
+ value changed since scheduling; an unrelated field's edit inside the
83
+ window still lets the pending validation run.
84
+ - **Array ops accept a null-valued nullable array.** `arrayPush` on a
85
+ `.nullable()` array field holding `null` warned and silently no-op'd,
86
+ while `undefined` created the array — even though the op item types
87
+ are built on `NonNullable<...>` precisely so nullable arrays accept
88
+ ops. Both empties now start the op from `[]`.
89
+
90
+ ### Changed
91
+
92
+ - **Awaited async validation is typed settled.** `validateAsync` and
93
+ `validateFieldAsync` (and `useField`'s `validateAsync`) resolved a
94
+ union carrying a `"pending"` arm that can never occur — the promise IS
95
+ the pending state — forcing every exhaustive `switch` to carry a dead
96
+ case. All three now resolve the `Settled*` result types
97
+ `validateFieldsAsync` always had. A `switch` that handled `"pending"`
98
+ on an awaited result will now fail to compile; delete the dead arm.
99
+
100
+ ## formstand-cli 0.16.0 — 2026-09-16
101
+
102
+ ### Added
103
+
104
+ - **Union array rows generate.** A discriminated union that is an
105
+ array's row item — the shape the airfield-maps trial hand-wrote — now
106
+ compiles to a `{Stem}Row` component per row instead of a TODO, in
107
+ both layouts: the discriminant and common fields bind with the field
108
+ hook, variant-only fields with the variant hook, all on the
109
+ row-indexed path (`methods.${index}`), with the same conditional
110
+ variant blocks, fragment elision, and hoisted number-props hooks a
111
+ static union gets. Needs formstand 0.16 for the row-indexed variant
112
+ path types. A union or tuple nested as a FIELD inside a row object,
113
+ and a tuple row item, keep their TODOs.
114
+
115
+ ## 0.16.0 — 2026-09-16
116
+
117
+ ### Added
118
+
119
+ - **`useFields`: composite fields.** One control backed by several schema
120
+ paths (a coordinate pair behind a DMS input, a range, a date span)
121
+ binds them in one call: `useFields(form, ["latitude", "longitude"])`
122
+ returns the position-typed tuple of full field bindings plus the
123
+ combined view — `error` merged in path order and deduped (a
124
+ cross-field `superRefine` stamping both paths reads once),
125
+ `firstError`, any-of-them `touched`/`dirty`/`isValidating`, and one
126
+ `onBlur` covering every path. The paths tuple must be render-stable;
127
+ React itself throws its hooks-count error otherwise, at the exact
128
+ render that broke the contract.
129
+ - **`field.firstError`.** The first error message or `undefined`, derived
130
+ on `UseFieldReturn` — the display shorthand every consumer (and every
131
+ generated kit adapter) re-derived by hand as `error?.[0]` with an
132
+ empty-array guard. `{field.firstError}` is now the whole helper line;
133
+ the full `field.error` array is unchanged beside it. The CLI keeps
134
+ emitting its own helper for now so generated code stays runnable on
135
+ older formstand; a future major of the generated adapters can lean on
136
+ this instead.
137
+
138
+ ### Fixed
139
+
140
+ - **`useVariantField` accepts row-indexed union paths.** A discriminated
141
+ union that is an array's row item was unbindable: `UnionValueAt`
142
+ resolved paths through `keyof` only, so any array-index segment (a
143
+ template `` `pavements.${number}` `` or a literal `"pavements.0"`)
144
+ collapsed the variant-key set to `never` and the `field` argument
145
+ rejected everything. The type machinery now resolves the array element
146
+ through numeric segments, so a row component binds
147
+ `useVariantField(form, `` `pavements.${index}` ``, "designator")` fully
148
+ typed, in both the plain and the `createFormHooks`-bound flavors.
149
+ Runtime behavior was always correct; only the types refused. Found by
150
+ the airfield-maps trial, whose generated TODO pointed at exactly this
151
+ call.
152
+
153
+ ## formstand-cli 0.15.0 — 2026-09-16
154
+
155
+ ### Changed
156
+
157
+ - **`--form-prop` is single-file only.** With `--layout module` the
158
+ fields are pre-wired to the module's own singleton, so the `form`
159
+ prop was decorative when it told the truth and silently split state
160
+ when it lied (the dev-mode guard existed to catch the lie). The CLI
161
+ now rejects the combination with the fix in the message: the page
162
+ already owns the module's form by importing the exported instance and
163
+ driving it directly. The wizard stops asking the question under the
164
+ module layout, and the playground's command builder disables the
165
+ checkbox. The programmatic `emitModuleForm` surface keeps the option
166
+ for callers who accept the singleton contract knowingly.
167
+
168
+ ### Fixed
169
+
170
+ - **The array-row union TODO gives honest advice.** The single-file
171
+ spelling claimed the union was "not supported"; the module spelling
172
+ recommended a `useVariantField` call that did not typecheck before the
173
+ library fix above. Both now describe the working binding and name the
174
+ formstand 0.16+ floor for row-indexed union paths.
175
+
176
+ ## formstand-cli 0.14.0 — 2026-09-10
177
+
178
+ ### Added
179
+
180
+ - **`component: "textarea"` in the config `fields` block.** The second
181
+ override flavor: a multi-line control for string fields, the binding
182
+ unchanged. Each kit renders its own control (mui `TextField multiline`,
183
+ mantine/chakra/shadcn `Textarea`, antd `Input.TextArea`, plain a native
184
+ `<textarea>`), and it composes with `span` in one entry, so a
185
+ `span: "full"` cover letter can finally look like one. `optionsProp`
186
+ and enum targets are rejected loudly.
187
+ - **JSON Schema defaults and string formats reach the generated
188
+ validator.** A document `default` now emits `.default(value)` in the
189
+ generated zod schema beside the `initialValues` seed it already fed,
190
+ and `format: email`/`uri`/`uuid` emits `z.email()`/`z.url()`/
191
+ `z.uuid()` while the control stays a text field. The emitted schema is
192
+ the runtime source of truth, so it no longer drops contracts the
193
+ source document carried. Note zod's `.default()` makes the input
194
+ optional by design.
195
+
196
+ ### Fixed
197
+
198
+ - **An optional union starts empty and its select can clear it.** An
199
+ optional/nullable discriminated union was seeded with its first
200
+ variant, so an untouched form submitted a variant the user never
201
+ chose, and the discriminant select offered no way back. The union now
202
+ starts `undefined`/`null`, and its select reads the discriminant but
203
+ writes the whole union: the empty choice clears it, a tag starts that
204
+ variant blank. mui's select gains an explicit None item; shadcn's
205
+ Radix select cannot hold an empty item, so there the fixed seed alone
206
+ closes the phantom-submit bug. Required unions are unchanged.
207
+ - **Array add buttons read as the action they perform.** "Add
208
+ references" becomes "Add reference": a conservative singularizer on
209
+ the label's last word, with irregulars passing through unchanged.
210
+ - **A union variant with one field no longer wraps it in a fragment.**
211
+
212
+ ## formstand-cli 0.13.0 — 2026-09-03
213
+
214
+ ### Added
215
+
216
+ - **JSON Schema and OpenAPI inputs.** A `.json` input file is read as a
217
+ bare JSON Schema (2020-12) or an OpenAPI 3.x document: a third
218
+ front-end into the same IR, following type mode's path, so the CLI
219
+ generates the zod schema beside the component (`--schema-out` places
220
+ it; `--layout module` makes it the module's `schema.ts`). The new
221
+ `--schema` flag selects inside the document: a component schema name,
222
+ or a full `#/...` JSON pointer that can reach an operation's request
223
+ body. What translates: `properties`/`required`, `enum`/`const`,
224
+ `oneOf` string consts, discriminated `oneOf` into `useVariantField`
225
+ unions, `allOf` object merging, `$ref` chains, `prefixItems` tuples,
226
+ `format: date`/`date-time` as date fields, primitive `default`s into
227
+ `initialValues`, `description` as helper text, `title` as the label,
228
+ and every nullable dialect (3.1 type arrays, 3.0 `nullable: true`,
229
+ `oneOf` with a null branch). Everything else degrades to the loud TODO
230
+ fallback; Swagger 2.0 and YAML are refused with conversion hints. The
231
+ wizard understands `.json` inputs too, asking `--schema` in place of
232
+ the zod-or-type question.
233
+
234
+ ### Fixed
235
+
236
+ - **A PascalCase schema export no longer collides with its module type
237
+ alias.** `export const MySchema` derives the form name MyForm, whose
238
+ schema type alias was also named MySchema, so the module barrel
239
+ exported two different MySchemas (the value from `schema.ts`, the type
240
+ from `types.ts`) and the generated `index.ts` failed to compile with
241
+ TS2308. The alias now yields the colliding name to the user's value
242
+ and becomes `MySchemaType`, both in `types.ts` and in the
243
+ `--form-prop` component that consumes it.
244
+
245
+ ## formstand-cli 0.12.0 — 2026-08-18
246
+
247
+ ### Added
248
+
249
+ - **`--wizard`: the flag questions asked one at a time.** Which file, zod
250
+ export or TS type, which kit, layout, section chrome, columns, name, and
251
+ where to write — Enter accepts the default on every question, invalid
252
+ answers re-ask, and an existing output target offers `--force` on the
253
+ spot. The interview ends by printing the composed `formstand-gen`
254
+ command before running it, so the run is reproducible without the wizard
255
+ from then on. Strictly opt-in (never a TTY sniff, and the flag runs
256
+ alone), prompts on stderr so stdout output stays pipeable, answers
257
+ pipeable on stdin, and zero new dependencies: numbered readline lists,
258
+ not a prompts library.
259
+ - **Per-field layout placement: `span` in the config `fields` block.** With
260
+ `--columns 2|3`, `"employment.notes": { span: "full" }` gives a field the
261
+ whole row and `{ span: 2 }` two of three columns; it composes with an
262
+ autocomplete override in the same entry. Each backend emits the span in
263
+ the dialect its section grids already use (mui `Grid` size objects, with
264
+ the legacy `item xs/sm` spelling on mui@5/6; mantine `Grid.Col` span
265
+ objects; antd `Col` `xs`/`sm`; shadcn `md:col-span-N`; chakra a `Box`
266
+ with responsive `gridColumn`), so every spelling still collapses to one
267
+ column on a phone. Plain is the one exception, for partial spans only:
268
+ inline styles cannot carry a media query, so a numeric span widens to the
269
+ full row with a comment saying so at the site. A span with no grid to act
270
+ on (a root-level field, array rows, a container, a 1-column form, or a
271
+ module-layout field below a section's direct children) is a loud
272
+ generation-time error, never a silent no-op.
273
+
274
+ ### Fixed
275
+
276
+ - **Generated bodies indent under their real container.** Panel and
277
+ collapsible chrome nests the body's container deeper than the section tag
278
+ (Card > CardContent > Grid), but cells were emitted shallower than the
279
+ container holding them and array row stacks sat beside their wrapper
280
+ cells instead of inside them. Both layouts now shift bodies to the
281
+ chrome's true depth, so generated files survive a host Prettier pass
282
+ without reflowing. Cosmetic only; the demos regenerate with a
283
+ whitespace-only diff.
284
+ - **A union riding a grid cell keeps its vertical rhythm.** Inside an
285
+ object section at `--columns 2|3`, a discriminated union's controls are
286
+ bare siblings in one full-row cell and had lost the spacing the 1-column
287
+ chrome provides; mui and mantine now wrap them in the kit's Stack. antd
288
+ needs no wrapper, since each of its controls is already a vertical Flex.
289
+
3
290
  ## 0.15.2 — 2026-08-11
4
291
 
5
292
  Identical to 0.15.1, republished under a fresh tag. The v0.15.1 tag was
@@ -1,9 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { StoreApi } from 'zustand/vanilla';
3
3
 
4
- type LeafType = string | number | boolean | bigint | symbol | null | undefined | Date | RegExp;
4
+ type BlobLike = Readonly<{
5
+ size: number;
6
+ type: string;
7
+ arrayBuffer: () => Promise<unknown>;
8
+ }>;
9
+ type LeafType = string | number | boolean | bigint | symbol | null | undefined | Date | RegExp | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | ((...args: never[]) => unknown) | BlobLike;
5
10
  type IsRecord<T> = T extends LeafType ? false : T extends readonly unknown[] ? false : T extends object ? true : false;
6
11
  type IsArray<T> = T extends readonly unknown[] ? true : false;
12
+ type IsTuple<T> = T extends readonly unknown[] ? number extends T["length"] ? false : true : false;
7
13
  type Prev = [
8
14
  never,
9
15
  0,
@@ -37,8 +43,10 @@ type PathDepth = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14
37
43
  type IsUnion<T, U = T> = [T] extends [never] ? false : T extends unknown ? [U] extends [T] ? false : true : never;
38
44
  type NormalizeDepth<D extends number> = number extends D ? DefaultPathDepth : [D] extends [PathDepth] ? IsUnion<D> extends true ? DefaultPathDepth : D : DefaultPathDepth;
39
45
  type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, NormalizeDepth<D>>;
40
- type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
- [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
46
+ type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsTuple<T> extends true ? {
47
+ [K in keyof T & `${number}`]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
48
+ }[keyof T & `${number}`] : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
49
+ [K in keyof T & string]: K extends `${string}.${string}` ? never : K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
50
  }[keyof T & string] : never;
43
51
  type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
52
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
@@ -114,6 +122,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
114
122
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
115
123
  type SubmitOptions = Readonly<{
116
124
  force?: boolean;
125
+ onError?: (error: unknown) => void;
117
126
  }>;
118
127
  type SubmitResult<TOutput> = Readonly<{
119
128
  kind: "valid";
@@ -159,6 +168,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
159
168
  setReValidateMode: (mode: ValidationMode) => void;
160
169
  setError: (path: ErrorPath<z.input<TSchema>, D>, errors: string | readonly string[]) => void;
161
170
  setErrors: (errors: ErrorMap) => void;
171
+ addErrors: (errors: ErrorMap) => void;
162
172
  clearErrors: (path?: ErrorPath<z.input<TSchema>, D>) => void;
163
173
  updateState: (updater: (state: FormState<z.input<TSchema>>) => Partial<Omit<FormState<z.input<TSchema>>, "errors">>) => void;
164
174
  reset: (nextInitial?: Partial<z.input<TSchema>>, options?: ResetOptions) => void;
@@ -168,8 +178,8 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
168
178
  validate: () => ValidationResult<z.output<TSchema>>;
169
179
  validateField: (path: ErrorPath<z.input<TSchema>, D>) => FieldValidationResult;
170
180
  validateFields: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => FieldsValidationResult;
171
- validateAsync: () => Promise<ValidationResult<z.output<TSchema>>>;
172
- validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<FieldValidationResult>;
181
+ validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
182
+ validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
173
183
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
174
184
  submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
175
185
  arrayPush: <P extends FieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
@@ -1,9 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { StoreApi } from 'zustand/vanilla';
3
3
 
4
- type LeafType = string | number | boolean | bigint | symbol | null | undefined | Date | RegExp;
4
+ type BlobLike = Readonly<{
5
+ size: number;
6
+ type: string;
7
+ arrayBuffer: () => Promise<unknown>;
8
+ }>;
9
+ type LeafType = string | number | boolean | bigint | symbol | null | undefined | Date | RegExp | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | ((...args: never[]) => unknown) | BlobLike;
5
10
  type IsRecord<T> = T extends LeafType ? false : T extends readonly unknown[] ? false : T extends object ? true : false;
6
11
  type IsArray<T> = T extends readonly unknown[] ? true : false;
12
+ type IsTuple<T> = T extends readonly unknown[] ? number extends T["length"] ? false : true : false;
7
13
  type Prev = [
8
14
  never,
9
15
  0,
@@ -37,8 +43,10 @@ type PathDepth = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14
37
43
  type IsUnion<T, U = T> = [T] extends [never] ? false : T extends unknown ? [U] extends [T] ? false : true : never;
38
44
  type NormalizeDepth<D extends number> = number extends D ? DefaultPathDepth : [D] extends [PathDepth] ? IsUnion<D> extends true ? DefaultPathDepth : D : DefaultPathDepth;
39
45
  type FieldPath<T, D extends number = DefaultPathDepth> = FieldPathRec<T, NormalizeDepth<D>>;
40
- type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
41
- [K in keyof T & string]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
46
+ type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsTuple<T> extends true ? {
47
+ [K in keyof T & `${number}`]: K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
48
+ }[keyof T & `${number}`] : IsArray<T> extends true ? T extends readonly (infer U)[] ? `${number}` | (IsRecord<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : IsArray<NonNullable<U>> extends true ? `${number}.${FieldPathRec<NonNullable<U>, Prev[D]>}` : never) : never : IsRecord<T> extends true ? {
49
+ [K in keyof T & string]: K extends `${string}.${string}` ? never : K | (IsRecord<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : IsArray<NonNullable<T[K]>> extends true ? `${K}.${FieldPathRec<NonNullable<T[K]>, Prev[D]>}` : never);
42
50
  }[keyof T & string] : never;
43
51
  type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
44
52
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
@@ -114,6 +122,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
114
122
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
115
123
  type SubmitOptions = Readonly<{
116
124
  force?: boolean;
125
+ onError?: (error: unknown) => void;
117
126
  }>;
118
127
  type SubmitResult<TOutput> = Readonly<{
119
128
  kind: "valid";
@@ -159,6 +168,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
159
168
  setReValidateMode: (mode: ValidationMode) => void;
160
169
  setError: (path: ErrorPath<z.input<TSchema>, D>, errors: string | readonly string[]) => void;
161
170
  setErrors: (errors: ErrorMap) => void;
171
+ addErrors: (errors: ErrorMap) => void;
162
172
  clearErrors: (path?: ErrorPath<z.input<TSchema>, D>) => void;
163
173
  updateState: (updater: (state: FormState<z.input<TSchema>>) => Partial<Omit<FormState<z.input<TSchema>>, "errors">>) => void;
164
174
  reset: (nextInitial?: Partial<z.input<TSchema>>, options?: ResetOptions) => void;
@@ -168,8 +178,8 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
168
178
  validate: () => ValidationResult<z.output<TSchema>>;
169
179
  validateField: (path: ErrorPath<z.input<TSchema>, D>) => FieldValidationResult;
170
180
  validateFields: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => FieldsValidationResult;
171
- validateAsync: () => Promise<ValidationResult<z.output<TSchema>>>;
172
- validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<FieldValidationResult>;
181
+ validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
182
+ validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
173
183
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
174
184
  submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
175
185
  arrayPush: <P extends FieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-jlzK2lCG.cjs';
3
+ import { F as Form } from '../createForm-D0xLZH-Q.cjs';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-jlzK2lCG.js';
3
+ import { F as Form } from '../createForm-D0xLZH-Q.js';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";