formstand 0.24.0 → 0.26.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,193 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0 — 2026-11-12
4
+
5
+ ### Added
6
+
7
+ - `PersistHandle.flush()` writes the pending debounced draft now, for
8
+ the moments that cannot wait out the debounce (a route change, a
9
+ `pagehide` handler). `dispose()` flushes too; see Fixed below.
10
+ - `submit` and `handleSubmit` take an options object in the second
11
+ position: `submit(onValid, { onInvalid, onError, force })`, the shape
12
+ `useFormAction`'s `FormActionOptions` always had, so `submit(onValid,
13
+ undefined, { force: true })` is no longer the only spelling.
14
+ `SubmitOptions` gains `onInvalid`; a positional `onInvalid` wins when
15
+ both are given. The positional shape stays.
16
+ - `ErrorPath<TValues, D>` is exported: the path type `setError`,
17
+ `clearErrors`, `validateField` and `validateFieldAsync` take
18
+ (`FieldPath` plus the root `""`), for wrappers that forward those
19
+ calls.
20
+ - `createFormHooks` binds the three hooks it skipped while the docs
21
+ promised every hook: `use{Name}Action` and `use{Name}ActionState`
22
+ (the form-actions bridges) and `use{Name}Persist` (`usePersistForm`),
23
+ with the `BoundUseAction`, `BoundUseActionState` and `BoundUsePersist`
24
+ types exported.
25
+ - `FieldSnapshot` (from `getFieldState` and `watchField`) carries
26
+ `initialValue` and `firstError`, matching `useField`.
27
+ - `fillField`, `blurField` and `fillAndBlurField` from `formstand/testing`
28
+ return the `FieldValidationResult` when the gate validated
29
+ (`undefined` when it did not), so a test can await a pending async
30
+ refine instead of guessing a delay.
31
+
32
+ ### Fixed
33
+
34
+ - A throwing async refinement (a rejected fetch inside `refine(async
35
+ ...)`) left `isValidatingForm` or `isValidating[path]` at `true` for
36
+ good: none of the async passes released their flag when the parse
37
+ rejected, so every spinner and submit gate built on them stuck after
38
+ one network failure. The passes now release the flags they own before
39
+ the rejection travels on to whoever awaited them. The passes nobody
40
+ awaits, `validateOnMount` and the change and blur triggers inside
41
+ `useField`, report the error through `console.error` instead of
42
+ leaving an unhandled rejection.
43
+ - Switching a discriminated union's variant through its discriminant
44
+ (`useField(form, "payment.method").setValue("paypal")`, which is how
45
+ generated forms bind it) left the old variant's schema errors in place
46
+ until a full `validate()` ran, so `useIsValid` read `false` for a form
47
+ the schema accepted. A field-scoped pass that runs the full parse (any
48
+ path under a union or a refined object) now also drops the schema
49
+ errors elsewhere that the parse no longer reports. It still adds
50
+ nothing outside its scope.
51
+ - `setErrors({ field: [] })`, an `updateState` patch of either error
52
+ channel, and `restore()` kept empty lists as entries, where `setError`
53
+ and `addErrors` dropped them, so one state read valid to `useIsValid`,
54
+ errored to `useFormSteps`, and `[]` from `useField(...).error`. Every
55
+ writer now drops an empty list.
56
+ - A throwing `onInvalid` resolved `{ kind: "error" }` without calling
57
+ `onError`; it does now, like a throwing `onValid`.
58
+ - `persistForm`: unmounting inside the debounce window lost the last
59
+ edits, because `dispose()` cancelled the pending write. It writes the
60
+ draft first now.
61
+ - `persistForm`: `clear()` followed by `reset()` (the usual after-submit
62
+ pair) wrote the fresh baseline straight back as a draft, which the
63
+ next visit then adopted over whatever initial values the app had
64
+ meanwhile computed. After `clear()`, a rebase that leaves the form
65
+ pristine no longer writes; the next real edit does.
66
+ - `persistForm`: a draft stored before the schema gained a key erased
67
+ that key's initial value from `values` and `initialValues`, so
68
+ `reset()` could not bring it back and the user saw zod's generic
69
+ message instead of yours. Required keys the draft lacks are now filled
70
+ from the initial values (nested objects included); optional keys stay
71
+ absent, since an optional the user cleared looks the same.
72
+ - `persistForm`'s shape guard refused legitimate drafts: `z.coerce.number()`
73
+ behind a `""` initial value, and unions whose variants shape the same
74
+ key differently. The guard now asks the schema whether a kind change
75
+ is legitimate at the path before refusing, and a dev build logs why a
76
+ draft that was present got ignored (version mismatch, shape conflict
77
+ and its path, corrupt JSON), where `load()` used to return a silent
78
+ `false`.
79
+ - `usePersistForm` ignored a change of `options.storage` and kept writing
80
+ to the first storage it saw; it resubscribes on it, as it does for
81
+ `key`.
82
+ - `DateField` and `dateInputProps` threw `value.getTime is not a
83
+ function` at render after a persisted draft loaded a `Date` back as
84
+ the string JSON made of it (persist does not revive Dates, by design),
85
+ and the rewritten draft made the crash stick across visits. The input
86
+ now shows the string's calendar date; the schema still rejects it at
87
+ validation.
88
+ - `useFormSteps.goTo(NaN)` (a `Number("")` from an empty option)
89
+ recursed without bound and froze the tab. A target that is not a
90
+ finite number now resolves the current index.
91
+ - `parseNumberText`, behind `NumberField`, `numberInputProps` and
92
+ `useNumberInput`, accepted every JavaScript number literal, so typing
93
+ `0x10` stored 16. It reads plain decimal notation only.
94
+ - `useMaskedInput` cleared a required string field to `undefined` when
95
+ its key was omitted from the initial values, because the initial
96
+ value's type was the only signal. The current value counts too, so the
97
+ field clears to `""` and reports your message instead of zod's.
98
+ - `useField`'s `debounceMs` also delayed the blur validation. Blur now
99
+ drops a pending timer and validates at once.
100
+ - `useFields` returned a fresh object, `fields` array and `onBlur` on
101
+ every render while its sibling hooks memoize; it keeps its identity
102
+ until one of its fields changes. A paths tuple that changes length now
103
+ throws the hook's own message, ahead of React's hooks-count error.
104
+ - `FormstandDevtools` labelled a server error as "(showing)" or "(behind
105
+ a schema error)" by walking the flat schema-error map as a path, so a
106
+ root verdict always read as behind and a nested key always as
107
+ showing. It reads the key directly now.
108
+ - `FormstandDevtools` threw on a `bigint` value (`z.bigint()`), in the
109
+ value preview and in the diff; a bigint renders as its literal
110
+ spelling (`10n`).
111
+ - `FormstandDevtools` walked every leaf through `getFieldState` and ran
112
+ `diff()` on every store write while collapsed; a collapsed panel now
113
+ keeps only its store subscription.
114
+
115
+ ## 0.25.0 — 2026-11-12
116
+
117
+ ### Added
118
+
119
+ - `useVariantFieldArray(form, unionPath, field)`, the array twin of
120
+ `useVariantField`: a `z.array` that lives in one variant of a
121
+ discriminated union sits at a path `useFieldArray` cannot take, since
122
+ `FieldPath` on a union offers only the keys every variant shares. The
123
+ hook binds `${unionPath}.${field}` and types the rows from the variants
124
+ that declare the array; `field` is a variant sub-path (`"lights"`,
125
+ `"spec.lights"`, or a row template for an array nested in another
126
+ variant array's rows), and a common-key array stays with plain
127
+ `useFieldArray`, which the variant hook rejects. Same return as
128
+ `useFieldArray`, row ids included. `createFormHooks` gains the bound
129
+ `use{Name}VariantFieldArray`, and the type `VariantArrayFieldPath` is
130
+ exported. formstand-cli emits it for arrays inside union variants.
131
+
132
+ ### Fixed
133
+
134
+ - `createFormHooks(createForm(schema, options), name)` with the form
135
+ created inline collapsed every depth-typed bound hook (`useXField`,
136
+ `useXFieldArray`, `useXIsDirty`) to a `never` path: the inner call
137
+ inferred its depth from the outer call's still-unresolved depth
138
+ parameter, so the depth became the whole budget union. `createForm` and
139
+ `useForm` now take their depth from `pathDepth` alone (`NoInfer` on the
140
+ returned form), so the inline shape types like the const-form shape the
141
+ docs show, and an explicit `pathDepth` still infers its literal.
142
+
143
+ ## formstand-cli 0.26.0 — 2026-11-12
144
+
145
+ ### Added
146
+
147
+ - An **array inside a union variant** generates instead of degrading to a
148
+ TODO, in both layouts and every kit: a `{Stem}Rows` component on
149
+ `useVariantFieldArray(form, unionPath, "lights")` (formstand 0.25), its
150
+ row leaves on the variant path (the Bound components in the single-file
151
+ layout, `useVariantField` on a row template in the module layout), the
152
+ empty item typed from the hook, and deeper lists under those rows bound
153
+ the same way. Under a union row item the row's own index renames to the
154
+ next hole for the extracted component, as nested lists already do. Still
155
+ a TODO: a union inside a variant, a union or tuple inside a variant
156
+ array's rows, and an array every variant shares (a plain path, left to
157
+ `useFieldArray`). Generated output with a variant array needs formstand
158
+ 0.25 or newer.
159
+
160
+ ### Changed
161
+
162
+ - **Arrays of flat object rows generate as tables.** Where every row
163
+ field is a scalar within the path budget and none carries a component
164
+ override, the array renders as a table in both layouts and every kit:
165
+ one column per row field with
166
+ the label in the header (a description becomes the header's title), a
167
+ `{Stem}Row` component per row binding each cell from hoisted `useField`
168
+ hooks (`useVariantField` for an array inside a union variant) and
169
+ naming the control through `aria-label`, a Remove column, and the
170
+ array-level error plus the Add button under the table. antd's
171
+ data-driven `Table` gets one cell component per column. Rows holding a
172
+ container, scalar rows and rows with an overridden field keep the
173
+ cards, and the new `fields: { "<path>": { layout: "cards" } }` opts a
174
+ flat array back out; `layout: "table"` asserts one and errors, naming
175
+ the row field, where it cannot be built.
176
+
177
+ ### Fixed
178
+
179
+ - The single-file layout's TODO for a container that still degrades inside
180
+ a row union's variant printed the hole-stripped naming path
181
+ (`"pavements.sub"`), which names no field a reader can find. It prints
182
+ the row template (`"pavements.${index}.sub"`), as the module layout
183
+ already did.
184
+ - Two usage walks passed the wrong budget when descending into an
185
+ array's rows, so anything two objects deep under a row was skipped: a
186
+ discriminated union there rendered its variant controls without
187
+ importing the prop builders they call, and a described leaf there was
188
+ handed a `description` prop the Bound components did not declare.
189
+ Both walks carry the real budget now.
190
+
3
191
  ## 0.24.0 — 2026-10-23
4
192
 
5
193
  ### Added
package/README.md CHANGED
@@ -123,7 +123,7 @@ PATCH-ready payload of exactly what changed.
123
123
  | Area | What's there | Docs |
124
124
  | --- | --- | --- |
125
125
  | Form instance | `createForm`, `setValue`/`setValues`, `reset`/`resetField`/`adoptValues`, `submit`/`handleSubmit`, `diff`/`dirtyFields`, `snapshot`/`restore`, array ops | [createForm & Form](https://scrumrot.github.io/formstand/documentation/api/) |
126
- | Hooks | `useForm`, `useField`, `useFieldArray`, `useVariantField`, `useFormSelector`, `useFormValues`, the flag hooks, `createFormContext`, `createFormHooks` | [Hooks](https://scrumrot.github.io/formstand/documentation/api/hooks) |
126
+ | Hooks | `useForm`, `useField`, `useFieldArray`, `useVariantField`, `useVariantFieldArray`, `useFormSelector`, `useFormValues`, the flag hooks, `createFormContext`, `createFormHooks` | [Hooks](https://scrumrot.github.io/formstand/documentation/api/hooks) |
127
127
  | Components | `TextField`, `NumberField`, `DateField`, `CheckboxField`, `SelectField`, the prop builders, `useNumberInput`, `focusField`/`focusFirstError` | [Components & bindings](https://scrumrot.github.io/formstand/documentation/api/components) |
128
128
  | Validation | five modes plus `reValidateMode`, field-scoped passes, async refines with race handling, per-field `debounceMs` | [Validation](https://scrumrot.github.io/formstand/documentation/validation) |
129
129
  | Utilities | `parsePath`/`getAtPath`/`setAtPath`, `validateSync`/`validateAsync`, `persistForm`, and the full type list | [Utilities & types](https://scrumrot.github.io/formstand/documentation/api/utilities) |
@@ -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;
@@ -188,21 +191,29 @@ 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>>;
203
214
  restore: (snapshot: FormState<z.input<TSchema>>) => void;
204
215
  }>;
205
- declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
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;
@@ -188,21 +191,29 @@ 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>>;
203
214
  restore: (snapshot: FormState<z.input<TSchema>>) => void;
204
215
  }>;
205
- declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
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,12 @@ var styles = {
163
129
  cursor: "pointer"
164
130
  }
165
131
  };
132
+ var jsonReplacer = (_key, value) => typeof value === "bigint" ? `${String(value)}n` : 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
+ const json = JSON.stringify(value, jsonReplacer);
170
138
  return json === void 0 ? String(value) : json.length > 60 ? `${json.slice(0, 57)}\u2026` : json;
171
139
  };
172
140
  var flags = (row) => [
@@ -193,14 +161,12 @@ var Section = ({
193
161
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.sectionTitle, children: title }),
194
162
  children
195
163
  ] });
196
- var DevtoolsPanel = ({
164
+ var OpenPanel = ({
197
165
  form,
198
- position = "bottom-right",
199
- defaultOpen = false,
200
- label = "formstand"
166
+ state,
167
+ label,
168
+ onClose
201
169
  }) => {
202
- const state = useDevtoolsState(form);
203
- const [open, setOpen] = react.useState(defaultOpen);
204
170
  const [held, setHeld] = react.useState(null);
205
171
  const rows = leafPaths(state.values).map((path) => {
206
172
  const field = form.getFieldState(path);
@@ -218,7 +184,7 @@ var DevtoolsPanel = ({
218
184
  rows.map((row) => row.path)
219
185
  );
220
186
  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: [
187
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.panel, children: [
222
188
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.header, children: [
223
189
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: label }),
224
190
  /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.muted, children: [
@@ -227,15 +193,7 @@ var DevtoolsPanel = ({
227
193
  state.isValidatingForm ? " \xB7 validating" : "",
228
194
  state.submitCount > 0 ? ` \xB7 submits ${state.submitCount}` : ""
229
195
  ] }),
230
- /* @__PURE__ */ jsxRuntime.jsx(
231
- "button",
232
- {
233
- type: "button",
234
- style: styles.button,
235
- onClick: () => setOpen(false),
236
- children: "close"
237
- }
238
- )
196
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: styles.button, onClick: onClose, children: "close" })
239
197
  ] }),
240
198
  /* @__PURE__ */ jsxRuntime.jsx(Section, { title: `fields (${rows.length})`, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: styles.table, children: [
241
199
  /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
@@ -274,10 +232,10 @@ var DevtoolsPanel = ({
274
232
  " ",
275
233
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.error, children: messages.join(", ") }),
276
234
  " ",
277
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: getAtPath(state.schemaErrors, key) === void 0 ? "(showing)" : "(behind a schema error)" })
235
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: Object.hasOwn(state.schemaErrors, key) ? "(behind a schema error)" : "(showing)" })
278
236
  ] }, key))
279
237
  ] }),
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) }) }),
238
+ /* @__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
239
  /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "time travel", children: [
282
240
  /* @__PURE__ */ jsxRuntime.jsx(
283
241
  "button",
@@ -304,7 +262,25 @@ var DevtoolsPanel = ({
304
262
  " ",
305
263
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: held === null ? "nothing held" : "snapshot held" })
306
264
  ] })
307
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
265
+ ] });
266
+ };
267
+ var DevtoolsPanel = ({
268
+ form,
269
+ position = "bottom-right",
270
+ defaultOpen = false,
271
+ label = "formstand"
272
+ }) => {
273
+ const state = useDevtoolsState(form);
274
+ const [open, setOpen] = react.useState(defaultOpen);
275
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...styles.root, ...anchorFor(position) }, children: open ? /* @__PURE__ */ jsxRuntime.jsx(
276
+ OpenPanel,
277
+ {
278
+ form,
279
+ state,
280
+ label,
281
+ onClose: () => setOpen(false)
282
+ }
283
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
308
284
  "button",
309
285
  {
310
286
  type: "button",