formstand 0.25.0 → 0.26.1

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,210 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.1 — 2026-11-13
4
+
5
+ ### Added
6
+
7
+ - `watchField` listeners receive the previous snapshot as a second
8
+ argument, `listener(snapshot, prev)`, the way `watchValue` passes the
9
+ previous value.
10
+ - `useFieldArray` returns `validateAsync()`, the awaitable twin of
11
+ `validate()`; `FieldArrayFormApi` gains the optional
12
+ `validateFieldAsync` member that backs it.
13
+ - `useVariantField` and `useFields` (and their bound forms from
14
+ `createFormHooks`) take the same `options` as `useField`, so a variant
15
+ or composite field can debounce its change validation.
16
+ - `FormActionOptions.force`, mirroring `SubmitOptions.force`.
17
+
18
+ ### Fixed
19
+
20
+ - `validateOnMount` judges every baseline the form takes: the pass runs
21
+ again after `reset()` (unless `keepErrors`) and after `adoptValues()`,
22
+ so a persisted draft adopted over the initial values no longer opens a
23
+ `useIsValid` gate for an invalid draft.
24
+ - `validateFieldsAsync` pruned live errors with a stale full-form map
25
+ when a batch mixed a full-parse path with an index into an empty array
26
+ and the values changed in flight; the prune now rides only while the
27
+ whole values reference held.
28
+ - `restore()` disowns in-flight async passes the way `reset()` and
29
+ `adoptValues()` do, so a pass started before the restore cannot commit
30
+ a verdict onto the restored state.
31
+ - `persistForm`'s gap fill fills a nullable-but-required key the draft
32
+ lacks, no longer splices one union variant's key into another, and no
33
+ longer resurrects a `z.record` key the draft removed.
34
+ - A manual `load()` no longer writes the draft it just read straight
35
+ back to storage.
36
+ - The auto-apply skips a draft the form already holds, so
37
+ `usePersistForm` with an inline `storage` object (a resubscribe per
38
+ render) no longer re-adopts the draft every render, wiping errors and
39
+ dirty state and looping with `useFormValues`; StrictMode's second
40
+ mount adopts nothing either.
41
+ - `useFieldArray`'s change and blur triggers report a throwing async
42
+ array refine through `console.error`, the way `useField`'s triggers
43
+ do, instead of leaving an unhandled rejection.
44
+ - `useFieldArray.update(index, item)` past the end (or with a
45
+ non-integer index) is refused with a warning, like the index ops; it
46
+ used to write `null` holes.
47
+ - `useFormSteps.next()` and `goTo()` judge a step on its live values: a
48
+ step value that changed while an async rule was in flight is validated
49
+ again instead of advancing on the stale verdict. With no steps defined
50
+ `next()` no longer sets the index to -1.
51
+ - `dateToInputText` reads a date-only string (a persisted draft's
52
+ `"2026-03-01"`) as the calendar date it spells; the Date constructor
53
+ parsed it as UTC midnight, which rendered the previous day west of UTC.
54
+ - The typed variant hooks bind a union nested inside another union's
55
+ variant (`useVariantField(form, "shape.detail", "x")`); `UnionValueAt`
56
+ walked common keys only, so neither spelling compiled.
57
+ - `CheckboxField` without a `label` no longer renders an empty `<label>`
58
+ around the input.
59
+ - The devtools panel keeps its held snapshot across a collapse (0.26.0
60
+ moved the held state into the open branch), and its diff shows a
61
+ field cleared to `undefined` as `"undefined"` instead of `{}`.
62
+ - `fillAndBlurField` returns the fill's result when the blur gate did
63
+ not validate (`mode: "onChange"`, or after a submit), instead of
64
+ `undefined` for a pass it had started.
65
+ - `persistForm`'s gap fill walks through lazy, intersection and
66
+ preprocess schemas again (the container check above stopped at them),
67
+ so a plain required key under those roots is filled as in 0.26.0.
68
+ - `useField`'s `emptyValue` and `clearable` for a key shared by union
69
+ variants follow the variant the live discriminant selects, so clearing
70
+ a nullable field on the active variant writes `null` instead of the
71
+ first variant's `undefined`.
72
+ - A write the path walker refuses (a string key into an array, a key
73
+ through a `Date`) no longer rebuilds the values spine, so watchers do
74
+ not fire, the op log is kept and spine server verdicts stay, matching
75
+ the "value left unchanged" warning.
76
+ - A field-scoped pass that ran the full parse takes the parse's current
77
+ message for an out-of-scope key it already held, so a cross-field rule
78
+ with a dynamic message no longer shows the stale text.
79
+ - `usePersistForm` forwards every storage call to the storage of the
80
+ latest render through a proxy instead of resubscribing when the
81
+ `storage` identity changes: an inline storage object resubscribed on
82
+ every render, and each outgoing subscription flushed, which collapsed
83
+ the debounce to nothing and re-read the draft each render.
84
+ - `useFormSteps` reads the step the user is on when `next()`, `back()`
85
+ or `goTo()` is called, not the step of the render the closure came
86
+ from, so two awaited `next()` calls from one handler advance twice and
87
+ a `back()` pressed during an in-flight advance wins at once. The three
88
+ are reference-stable across renders now.
89
+ - `dateToInputText` renders `""` for a date-shaped string that spells no
90
+ real day (`"2026-02-31"`) instead of the Date constructor's reading.
91
+ - The devtools panel renders `NaN` and the infinities as their own
92
+ spelling in the fields table and the diff instead of `null`, and
93
+ forgets its held snapshot when the `form` prop changes to another
94
+ form instead of restoring the old form's state into the new one.
95
+
96
+ ## 0.26.0 — 2026-11-12
97
+
98
+ ### Added
99
+
100
+ - `PersistHandle.flush()` writes the pending debounced draft now, for
101
+ the moments that cannot wait out the debounce (a route change, a
102
+ `pagehide` handler). `dispose()` flushes too; see Fixed below.
103
+ - `submit` and `handleSubmit` take an options object in the second
104
+ position: `submit(onValid, { onInvalid, onError, force })`, the shape
105
+ `useFormAction`'s `FormActionOptions` always had, so `submit(onValid,
106
+ undefined, { force: true })` is no longer the only spelling.
107
+ `SubmitOptions` gains `onInvalid`; a positional `onInvalid` wins when
108
+ both are given. The positional shape stays.
109
+ - `ErrorPath<TValues, D>` is exported: the path type `setError`,
110
+ `clearErrors`, `validateField` and `validateFieldAsync` take
111
+ (`FieldPath` plus the root `""`), for wrappers that forward those
112
+ calls.
113
+ - `createFormHooks` binds the three hooks it skipped while the docs
114
+ promised every hook: `use{Name}Action` and `use{Name}ActionState`
115
+ (the form-actions bridges) and `use{Name}Persist` (`usePersistForm`),
116
+ with the `BoundUseAction`, `BoundUseActionState` and `BoundUsePersist`
117
+ types exported.
118
+ - `FieldSnapshot` (from `getFieldState` and `watchField`) carries
119
+ `initialValue` and `firstError`, matching `useField`.
120
+ - `fillField`, `blurField` and `fillAndBlurField` from `formstand/testing`
121
+ return the `FieldValidationResult` when the gate validated
122
+ (`undefined` when it did not), so a test can await a pending async
123
+ refine instead of guessing a delay.
124
+
125
+ ### Fixed
126
+
127
+ - A throwing async refinement (a rejected fetch inside `refine(async
128
+ ...)`) left `isValidatingForm` or `isValidating[path]` at `true` for
129
+ good: none of the async passes released their flag when the parse
130
+ rejected, so every spinner and submit gate built on them stuck after
131
+ one network failure. The passes now release the flags they own before
132
+ the rejection travels on to whoever awaited them. The passes nobody
133
+ awaits, `validateOnMount` and the change and blur triggers inside
134
+ `useField`, report the error through `console.error` instead of
135
+ leaving an unhandled rejection.
136
+ - Switching a discriminated union's variant through its discriminant
137
+ (`useField(form, "payment.method").setValue("paypal")`, which is how
138
+ generated forms bind it) left the old variant's schema errors in place
139
+ until a full `validate()` ran, so `useIsValid` read `false` for a form
140
+ the schema accepted. A field-scoped pass that runs the full parse (any
141
+ path under a union or a refined object) now also drops the schema
142
+ errors elsewhere that the parse no longer reports. It still adds
143
+ nothing outside its scope.
144
+ - `setErrors({ field: [] })`, an `updateState` patch of either error
145
+ channel, and `restore()` kept empty lists as entries, where `setError`
146
+ and `addErrors` dropped them, so one state read valid to `useIsValid`,
147
+ errored to `useFormSteps`, and `[]` from `useField(...).error`. Every
148
+ writer now drops an empty list.
149
+ - A throwing `onInvalid` resolved `{ kind: "error" }` without calling
150
+ `onError`; it does now, like a throwing `onValid`.
151
+ - `persistForm`: unmounting inside the debounce window lost the last
152
+ edits, because `dispose()` cancelled the pending write. It writes the
153
+ draft first now.
154
+ - `persistForm`: `clear()` followed by `reset()` (the usual after-submit
155
+ pair) wrote the fresh baseline straight back as a draft, which the
156
+ next visit then adopted over whatever initial values the app had
157
+ meanwhile computed. After `clear()`, a rebase that leaves the form
158
+ pristine no longer writes; the next real edit does.
159
+ - `persistForm`: a draft stored before the schema gained a key erased
160
+ that key's initial value from `values` and `initialValues`, so
161
+ `reset()` could not bring it back and the user saw zod's generic
162
+ message instead of yours. Required keys the draft lacks are now filled
163
+ from the initial values (nested objects included); optional keys stay
164
+ absent, since an optional the user cleared looks the same.
165
+ - `persistForm`'s shape guard refused legitimate drafts: `z.coerce.number()`
166
+ behind a `""` initial value, and unions whose variants shape the same
167
+ key differently. The guard now asks the schema whether a kind change
168
+ is legitimate at the path before refusing, and a dev build logs why a
169
+ draft that was present got ignored (version mismatch, shape conflict
170
+ and its path, corrupt JSON), where `load()` used to return a silent
171
+ `false`.
172
+ - `usePersistForm` ignored a change of `options.storage` and kept writing
173
+ to the first storage it saw; it resubscribes on it, as it does for
174
+ `key`.
175
+ - `DateField` and `dateInputProps` threw `value.getTime is not a
176
+ function` at render after a persisted draft loaded a `Date` back as
177
+ the string JSON made of it (persist does not revive Dates, by design),
178
+ and the rewritten draft made the crash stick across visits. The input
179
+ now shows the string's calendar date; the schema still rejects it at
180
+ validation.
181
+ - `useFormSteps.goTo(NaN)` (a `Number("")` from an empty option)
182
+ recursed without bound and froze the tab. A target that is not a
183
+ finite number now resolves the current index.
184
+ - `parseNumberText`, behind `NumberField`, `numberInputProps` and
185
+ `useNumberInput`, accepted every JavaScript number literal, so typing
186
+ `0x10` stored 16. It reads plain decimal notation only.
187
+ - `useMaskedInput` cleared a required string field to `undefined` when
188
+ its key was omitted from the initial values, because the initial
189
+ value's type was the only signal. The current value counts too, so the
190
+ field clears to `""` and reports your message instead of zod's.
191
+ - `useField`'s `debounceMs` also delayed the blur validation. Blur now
192
+ drops a pending timer and validates at once.
193
+ - `useFields` returned a fresh object, `fields` array and `onBlur` on
194
+ every render while its sibling hooks memoize; it keeps its identity
195
+ until one of its fields changes. A paths tuple that changes length now
196
+ throws the hook's own message, ahead of React's hooks-count error.
197
+ - `FormstandDevtools` labelled a server error as "(showing)" or "(behind
198
+ a schema error)" by walking the flat schema-error map as a path, so a
199
+ root verdict always read as behind and a nested key always as
200
+ showing. It reads the key directly now.
201
+ - `FormstandDevtools` threw on a `bigint` value (`z.bigint()`), in the
202
+ value preview and in the diff; a bigint renders as its literal
203
+ spelling (`10n`).
204
+ - `FormstandDevtools` walked every leaf through `getFieldState` and ran
205
+ `diff()` on every store write while collapsed; a collapsed panel now
206
+ keeps only its store subscription.
207
+
3
208
  ## 0.25.0 — 2026-11-12
4
209
 
5
210
  ### Added
@@ -0,0 +1,22 @@
1
+ import { useStore } from 'zustand/react';
2
+ import { useShallow } from 'zustand/react/shallow';
3
+
4
+ // src/react/useFormSelector.ts
5
+ function useFormSelector(form, selector) {
6
+ return useStore(form.store, selector);
7
+ }
8
+ function useFormSelectorShallow(form, selector) {
9
+ return useStore(form.store, useShallow(selector));
10
+ }
11
+
12
+ // src/core/env.ts
13
+ var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
14
+ var isDevToolsBuild = () => {
15
+ if (typeof process === "undefined") return false;
16
+ const env = process.env?.["NODE_ENV"];
17
+ return typeof env === "string" && env !== "production";
18
+ };
19
+
20
+ export { isDevBuild, isDevToolsBuild, useFormSelector, useFormSelectorShallow };
21
+ //# sourceMappingURL=chunk-XJ6W5CKB.js.map
22
+ //# sourceMappingURL=chunk-XJ6W5CKB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/useFormSelector.ts","../src/core/env.ts"],"names":[],"mappings":";;;;AAiCO,SAAS,eAAA,CACd,MACA,QAAA,EACG;AACH,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,KAAA,EAAO,QAAQ,CAAA;AACtC;AAcO,SAAS,sBAAA,CACd,MACA,QAAA,EACG;AACH,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,KAAA,EAAO,UAAA,CAAW,QAAQ,CAAC,CAAA;AAClD;;;AC/CO,IAAM,UAAA,GAAa,MACxB,OAAO,OAAA,KAAY,eACnB,OAAA,CAAQ,GAAA,GAAM,UAAU,CAAA,KAAM;AAWzB,IAAM,kBAAkB,MAAe;AAC5C,EAAA,IAAI,OAAO,OAAA,KAAY,WAAA,EAAa,OAAO,KAAA;AAC3C,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,GAAM,UAAU,CAAA;AACpC,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,YAAA;AAC5C","file":"chunk-XJ6W5CKB.js","sourcesContent":["import type { z } from \"zod\";\nimport type { StoreApi } from \"zustand/vanilla\";\nimport { useStore } from \"zustand/react\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport type { Form } from \"../core/createForm\";\nimport type { DefaultPathDepth, PathDepth } from \"../core/fieldPath\";\nimport type { FormState } from \"../core/types\";\n\ntype ReadonlyStore<T> = Pick<\n StoreApi<T>,\n \"getState\" | \"getInitialState\" | \"subscribe\"\n>;\n\nexport type FormStateApi = Readonly<{\n store: ReadonlyStore<FormState<unknown>>;\n}>;\n\nexport function useFormSelector<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\n// The `schema?: undefined` brand forces Form<TSchema> (which has a real\n// `schema` property) to bind only the typed overload above. Without\n// it, Form<TSchema> would structurally satisfy FormStateApi too and a\n// bad selector could silently widen to FormState<unknown>.\nexport function useFormSelector<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelector<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, selector);\n}\n\nexport function useFormSelectorShallow<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, useShallow(selector));\n}\n","// The dev-build test the library consults before emitting a console\n// warning. Bundlers that define NODE_ENV fold it to a constant and drop the\n// dev-only branches; the `typeof process` guard keeps the SAME expression\n// from throwing in a runtime that defines no `process` at all (a CDN/ESM\n// consumer, a test runner without the define). That last case matters most\n// on the submit path: handleSubmit promises never to reject, and the\n// warning that fires only when a save failed was the one line that could\n// break the promise exactly on the failure path. It FAILS OPEN on purpose:\n// an unknowable environment gets the warnings, which cost nothing but a\n// console line.\nexport const isDevBuild = (): boolean =>\n typeof process === \"undefined\" ||\n process.env?.[\"NODE_ENV\"] !== \"production\";\n\n// The gate for the two sites that EXPOSE form state: the FormstandDevtools\n// panel (renders every value and error into the page) and the zustand\n// devtools middleware (streams every write to the Redux extension). These\n// must FAIL CLOSED: isDevBuild's \"no process means dev\" reading let a\n// CDN/ESM, Deno, or worker production build mount the panel and keep the\n// middleware live for any end user with the extension installed. Only a\n// readable NODE_ENV that is a string other than \"production\" counts as\n// development; no process, no env, or an unset NODE_ENV all read as\n// production here.\nexport const isDevToolsBuild = (): boolean => {\n if (typeof process === \"undefined\") return false;\n const env = process.env?.[\"NODE_ENV\"];\n return typeof env === \"string\" && env !== \"production\";\n};\n"]}
@@ -130,6 +130,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
130
130
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
131
131
  type SubmitOptions = Readonly<{
132
132
  force?: boolean;
133
+ onInvalid?: InvalidSubmitHandler;
133
134
  onError?: (error: unknown) => void;
134
135
  }>;
135
136
  type SubmitResult<TOutput> = Readonly<{
@@ -152,7 +153,9 @@ type ResetOptions = Readonly<{
152
153
  }>;
153
154
  type FieldSnapshot<TValue> = Readonly<{
154
155
  value: TValue;
156
+ initialValue: TValue;
155
157
  error: readonly string[] | undefined;
158
+ firstError: string | undefined;
156
159
  touched: boolean;
157
160
  dirty: boolean;
158
161
  isValidating: boolean;
@@ -164,7 +167,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
164
167
  getState: () => FormState<z.input<TSchema>>;
165
168
  subscribe: (listener: (state: FormState<z.input<TSchema>>, prev: FormState<z.input<TSchema>>) => void) => () => void;
166
169
  getField: <P extends FieldPath<z.input<TSchema>, D>>(path: P) => FieldValue<z.input<TSchema>, P>;
167
- watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
170
+ watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>, previous: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
168
171
  watchValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (value: FieldValue<z.input<TSchema>, P>, previous: FieldValue<z.input<TSchema>, P>) => void) => () => void;
169
172
  watchValues: (listener: (values: z.input<TSchema>, previous: z.input<TSchema>) => void) => () => void;
170
173
  setValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, value: FieldValue<z.input<TSchema>, P>) => void;
@@ -188,15 +191,23 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
188
191
  validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
189
192
  validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
190
193
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
191
- submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
194
+ submit: {
195
+ (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
196
+ (onValid: SubmitHandler<TSchema>, options: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
197
+ };
192
198
  arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
193
199
  arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
194
200
  arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
195
201
  arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
196
202
  arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
197
- handleSubmit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => (event?: {
198
- preventDefault: () => void;
199
- }) => Promise<SubmitResult<z.output<TSchema>>>;
203
+ handleSubmit: {
204
+ (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): (event?: {
205
+ preventDefault: () => void;
206
+ }) => Promise<SubmitResult<z.output<TSchema>>>;
207
+ (onValid: SubmitHandler<TSchema>, options: SubmitOptions): (event?: {
208
+ preventDefault: () => void;
209
+ }) => Promise<SubmitResult<z.output<TSchema>>>;
210
+ };
200
211
  diff: () => Readonly<Record<string, unknown>>;
201
212
  dirtyFields: () => readonly string[];
202
213
  snapshot: () => FormState<z.input<TSchema>>;
@@ -204,5 +215,5 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
204
215
  }>;
205
216
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, NoInfer<D>>;
206
217
 
207
- export { createForm as p, emptyValueForSchema as q, flattenIssues as r, isAsyncRequiredError as s, isClearableSchema as t, shouldValidateOn as u, validateAsync as v, validateSync as w };
208
- export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, FieldSnapshot as f, FieldsValidationResult as g, ResetOptions as h, SettledFieldsValidationResult as i, SettledValidationResult as j, SubmitHandler as k, SubmitOptions as l, SubmitResult as m, ValidationResult as n, ValidationTrigger as o };
218
+ export { createForm as q, emptyValueForSchema as r, flattenIssues as s, isAsyncRequiredError as t, isClearableSchema as u, shouldValidateOn as v, validateAsync as w, validateSync as x };
219
+ export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, ErrorPath as f, FieldSnapshot as g, FieldsValidationResult as h, ResetOptions as i, SettledFieldsValidationResult as j, SettledValidationResult as k, SubmitHandler as l, SubmitOptions as m, SubmitResult as n, ValidationResult as o, ValidationTrigger as p };
@@ -130,6 +130,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
130
130
  type InvalidSubmitHandler = (errors: ErrorMap) => void;
131
131
  type SubmitOptions = Readonly<{
132
132
  force?: boolean;
133
+ onInvalid?: InvalidSubmitHandler;
133
134
  onError?: (error: unknown) => void;
134
135
  }>;
135
136
  type SubmitResult<TOutput> = Readonly<{
@@ -152,7 +153,9 @@ type ResetOptions = Readonly<{
152
153
  }>;
153
154
  type FieldSnapshot<TValue> = Readonly<{
154
155
  value: TValue;
156
+ initialValue: TValue;
155
157
  error: readonly string[] | undefined;
158
+ firstError: string | undefined;
156
159
  touched: boolean;
157
160
  dirty: boolean;
158
161
  isValidating: boolean;
@@ -164,7 +167,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
164
167
  getState: () => FormState<z.input<TSchema>>;
165
168
  subscribe: (listener: (state: FormState<z.input<TSchema>>, prev: FormState<z.input<TSchema>>) => void) => () => void;
166
169
  getField: <P extends FieldPath<z.input<TSchema>, D>>(path: P) => FieldValue<z.input<TSchema>, P>;
167
- watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
170
+ watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>, previous: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
168
171
  watchValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (value: FieldValue<z.input<TSchema>, P>, previous: FieldValue<z.input<TSchema>, P>) => void) => () => void;
169
172
  watchValues: (listener: (values: z.input<TSchema>, previous: z.input<TSchema>) => void) => () => void;
170
173
  setValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, value: FieldValue<z.input<TSchema>, P>) => void;
@@ -188,15 +191,23 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
188
191
  validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
189
192
  validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
190
193
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
191
- submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
194
+ submit: {
195
+ (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
196
+ (onValid: SubmitHandler<TSchema>, options: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
197
+ };
192
198
  arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
193
199
  arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
194
200
  arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
195
201
  arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
196
202
  arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
197
- handleSubmit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => (event?: {
198
- preventDefault: () => void;
199
- }) => Promise<SubmitResult<z.output<TSchema>>>;
203
+ handleSubmit: {
204
+ (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): (event?: {
205
+ preventDefault: () => void;
206
+ }) => Promise<SubmitResult<z.output<TSchema>>>;
207
+ (onValid: SubmitHandler<TSchema>, options: SubmitOptions): (event?: {
208
+ preventDefault: () => void;
209
+ }) => Promise<SubmitResult<z.output<TSchema>>>;
210
+ };
200
211
  diff: () => Readonly<Record<string, unknown>>;
201
212
  dirtyFields: () => readonly string[];
202
213
  snapshot: () => FormState<z.input<TSchema>>;
@@ -204,5 +215,5 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
204
215
  }>;
205
216
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, NoInfer<D>>;
206
217
 
207
- export { createForm as p, emptyValueForSchema as q, flattenIssues as r, isAsyncRequiredError as s, isClearableSchema as t, shouldValidateOn as u, validateAsync as v, validateSync as w };
208
- export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, FieldSnapshot as f, FieldsValidationResult as g, ResetOptions as h, SettledFieldsValidationResult as i, SettledValidationResult as j, SubmitHandler as k, SubmitOptions as l, SubmitResult as m, ValidationResult as n, ValidationTrigger as o };
218
+ export { createForm as q, emptyValueForSchema as r, flattenIssues as s, isAsyncRequiredError as t, isClearableSchema as u, shouldValidateOn as v, validateAsync as w, validateSync as x };
219
+ export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, ErrorPath as f, FieldSnapshot as g, FieldsValidationResult as h, ResetOptions as i, SettledFieldsValidationResult as j, SettledValidationResult as k, SubmitHandler as l, SubmitOptions as m, SubmitResult as n, ValidationResult as o, ValidationTrigger as p };
@@ -13,40 +13,6 @@ var isDevToolsBuild = () => {
13
13
  const env = process.env?.["NODE_ENV"];
14
14
  return typeof env === "string" && env !== "production";
15
15
  };
16
-
17
- // src/core/path.ts
18
- var isIndexSegment = (s) => {
19
- const n = Number(s);
20
- return Number.isInteger(n) && n >= 0 && String(n) === s;
21
- };
22
- var PARSE_CACHE_MAX = 4096;
23
- var parseCache = /* @__PURE__ */ new Map();
24
- var parsePath = (path) => {
25
- const cached = parseCache.get(path);
26
- if (cached !== void 0) {
27
- parseCache.delete(path);
28
- parseCache.set(path, cached);
29
- return cached;
30
- }
31
- const segments = Object.freeze(
32
- path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
33
- );
34
- if (parseCache.size >= PARSE_CACHE_MAX) {
35
- const oldest = parseCache.keys().next().value;
36
- if (oldest !== void 0) parseCache.delete(oldest);
37
- }
38
- parseCache.set(path, segments);
39
- return segments;
40
- };
41
- var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
42
- var readSegment = (obj, segment) => {
43
- if (obj == null) return void 0;
44
- if (Array.isArray(obj)) {
45
- return typeof segment === "number" ? obj[segment] : void 0;
46
- }
47
- return isPlainRecord(obj) && Object.hasOwn(obj, String(segment)) ? obj[String(segment)] : void 0;
48
- };
49
- var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
50
16
  function useFormSelectorShallow(form, selector) {
51
17
  return react$1.useStore(form.store, shallow.useShallow(selector));
52
18
  }
@@ -163,10 +129,15 @@ var styles = {
163
129
  cursor: "pointer"
164
130
  }
165
131
  };
132
+ var jsonReplacer = (_key, value) => typeof value === "bigint" ? `${String(value)}n` : value === void 0 ? "undefined" : typeof value === "number" && !Number.isFinite(value) ? String(value) : value;
166
133
  var preview = (value) => {
167
134
  if (value === void 0) return "undefined";
168
135
  if (value instanceof Date) return value.toISOString();
169
- const json = JSON.stringify(value);
136
+ if (typeof value === "bigint") return `${String(value)}n`;
137
+ if (typeof value === "number" && !Number.isFinite(value)) {
138
+ return String(value);
139
+ }
140
+ const json = JSON.stringify(value, jsonReplacer);
170
141
  return json === void 0 ? String(value) : json.length > 60 ? `${json.slice(0, 57)}\u2026` : json;
171
142
  };
172
143
  var flags = (row) => [
@@ -193,15 +164,14 @@ var Section = ({
193
164
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.sectionTitle, children: title }),
194
165
  children
195
166
  ] });
196
- var DevtoolsPanel = ({
167
+ var OpenPanel = ({
197
168
  form,
198
- position = "bottom-right",
199
- defaultOpen = false,
200
- label = "formstand"
169
+ state,
170
+ label,
171
+ held,
172
+ onHold,
173
+ onClose
201
174
  }) => {
202
- const state = useDevtoolsState(form);
203
- const [open, setOpen] = react.useState(defaultOpen);
204
- const [held, setHeld] = react.useState(null);
205
175
  const rows = leafPaths(state.values).map((path) => {
206
176
  const field = form.getFieldState(path);
207
177
  return {
@@ -218,7 +188,7 @@ var DevtoolsPanel = ({
218
188
  rows.map((row) => row.path)
219
189
  );
220
190
  const diff = form.diff();
221
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...styles.root, ...anchorFor(position) }, children: open ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.panel, children: [
191
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.panel, children: [
222
192
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.header, children: [
223
193
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: label }),
224
194
  /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.muted, children: [
@@ -227,15 +197,7 @@ var DevtoolsPanel = ({
227
197
  state.isValidatingForm ? " \xB7 validating" : "",
228
198
  state.submitCount > 0 ? ` \xB7 submits ${state.submitCount}` : ""
229
199
  ] }),
230
- /* @__PURE__ */ jsxRuntime.jsx(
231
- "button",
232
- {
233
- type: "button",
234
- style: styles.button,
235
- onClick: () => setOpen(false),
236
- children: "close"
237
- }
238
- )
200
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: styles.button, onClick: onClose, children: "close" })
239
201
  ] }),
240
202
  /* @__PURE__ */ jsxRuntime.jsx(Section, { title: `fields (${rows.length})`, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: styles.table, children: [
241
203
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
@@ -274,17 +236,17 @@ var DevtoolsPanel = ({
274
236
  " ",
275
237
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.error, children: messages.join(", ") }),
276
238
  " ",
277
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: getAtPath(state.schemaErrors, key) === void 0 ? "(showing)" : "(behind a schema error)" })
239
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: Object.hasOwn(state.schemaErrors, key) ? "(behind a schema error)" : "(showing)" })
278
240
  ] }, key))
279
241
  ] }),
280
- /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "diff vs initial", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { style: styles.pre, children: Object.keys(diff).length === 0 ? "clean" : JSON.stringify(diff, null, 2) }) }),
242
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "diff vs initial", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { style: styles.pre, children: Object.keys(diff).length === 0 ? "clean" : JSON.stringify(diff, jsonReplacer, 2) }) }),
281
243
  /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "time travel", children: [
282
244
  /* @__PURE__ */ jsxRuntime.jsx(
283
245
  "button",
284
246
  {
285
247
  type: "button",
286
248
  style: styles.button,
287
- onClick: () => setHeld(form.snapshot()),
249
+ onClick: () => onHold(form.snapshot()),
288
250
  children: "snapshot"
289
251
  }
290
252
  ),
@@ -304,7 +266,29 @@ var DevtoolsPanel = ({
304
266
  " ",
305
267
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: held === null ? "nothing held" : "snapshot held" })
306
268
  ] })
307
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
269
+ ] });
270
+ };
271
+ var DevtoolsPanel = ({
272
+ form,
273
+ position = "bottom-right",
274
+ defaultOpen = false,
275
+ label = "formstand"
276
+ }) => {
277
+ const state = useDevtoolsState(form);
278
+ const [open, setOpen] = react.useState(defaultOpen);
279
+ const [held, setHeld] = react.useState(null);
280
+ const heldForThisForm = held !== null && held.form === form ? held.snapshot : null;
281
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...styles.root, ...anchorFor(position) }, children: open ? /* @__PURE__ */ jsxRuntime.jsx(
282
+ OpenPanel,
283
+ {
284
+ form,
285
+ state,
286
+ label,
287
+ held: heldForThisForm,
288
+ onHold: (snapshot) => setHeld({ form, snapshot }),
289
+ onClose: () => setOpen(false)
290
+ }
291
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
308
292
  "button",
309
293
  {
310
294
  type: "button",