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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { isDevToolsBuild, getAtPath, isDevBuild, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-EQIS5IVH.js';
2
- export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-EQIS5IVH.js';
1
+ import { isDevToolsBuild, isDevBuild, useFormSelectorShallow, useFormSelector } from './chunk-XJ6W5CKB.js';
2
+ export { useFormSelector, useFormSelectorShallow } from './chunk-XJ6W5CKB.js';
3
3
  import { flagAt, entryAt, shouldValidateOn } from './chunk-V3PDMTPP.js';
4
4
  export { shouldValidateOn } from './chunk-V3PDMTPP.js';
5
5
  import { createStore } from 'zustand/vanilla';
@@ -103,6 +103,158 @@ var clearArrayOpsUnder = (store, path, keepSelf = false) => {
103
103
  );
104
104
  related.forEach((p) => byPath.delete(p));
105
105
  };
106
+
107
+ // src/core/equality.ts
108
+ var isPlainObject = (v) => {
109
+ if (typeof v !== "object" || v === null) return false;
110
+ const proto = Object.getPrototypeOf(v);
111
+ return proto === Object.prototype || proto === null;
112
+ };
113
+ var valuesEqual = (a, b) => {
114
+ if (Object.is(a, b)) return true;
115
+ if (typeof a === "number" && typeof b === "number") return a === b;
116
+ if (a instanceof Date && b instanceof Date) {
117
+ const [ta, tb] = [a.getTime(), b.getTime()];
118
+ return ta === tb || Number.isNaN(ta) && Number.isNaN(tb);
119
+ }
120
+ if (Array.isArray(a) && Array.isArray(b)) {
121
+ return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
122
+ }
123
+ if (isPlainObject(a) && isPlainObject(b)) {
124
+ const aKeys = Object.keys(a);
125
+ const bKeys = Object.keys(b);
126
+ return aKeys.length === bKeys.length && aKeys.every((k) => Object.hasOwn(b, k) && valuesEqual(a[k], b[k]));
127
+ }
128
+ return false;
129
+ };
130
+ var dirtyCache = /* @__PURE__ */ new WeakMap();
131
+ var MAX_VERDICTS = 4;
132
+ var isFieldDirty = (value, initialValue) => {
133
+ if (Object.is(value, initialValue)) return false;
134
+ if (typeof value !== "object" || value === null) {
135
+ return !valuesEqual(value, initialValue);
136
+ }
137
+ const cached = dirtyCache.get(value) ?? [];
138
+ const hit = cached.find((v) => Object.is(v.initial, initialValue));
139
+ if (hit !== void 0) return hit.dirty;
140
+ const dirty = !valuesEqual(value, initialValue);
141
+ dirtyCache.set(
142
+ value,
143
+ [{ initial: initialValue, dirty }, ...cached].slice(0, MAX_VERDICTS)
144
+ );
145
+ return dirty;
146
+ };
147
+
148
+ // src/core/path.ts
149
+ var MAX_ARRAY_INDEX = 1e5;
150
+ var isIndexSegment = (s) => {
151
+ const n = Number(s);
152
+ return Number.isInteger(n) && n >= 0 && String(n) === s;
153
+ };
154
+ var PARSE_CACHE_MAX = 4096;
155
+ var parseCache = /* @__PURE__ */ new Map();
156
+ var parsePath = (path) => {
157
+ const cached = parseCache.get(path);
158
+ if (cached !== void 0) {
159
+ parseCache.delete(path);
160
+ parseCache.set(path, cached);
161
+ return cached;
162
+ }
163
+ const segments = Object.freeze(
164
+ path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
165
+ );
166
+ if (parseCache.size >= PARSE_CACHE_MAX) {
167
+ const oldest = parseCache.keys().next().value;
168
+ if (oldest !== void 0) parseCache.delete(oldest);
169
+ }
170
+ parseCache.set(path, segments);
171
+ return segments;
172
+ };
173
+ var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
174
+ var readSegment = (obj, segment) => {
175
+ if (obj == null) return void 0;
176
+ if (Array.isArray(obj)) {
177
+ return typeof segment === "number" ? obj[segment] : void 0;
178
+ }
179
+ return isPlainRecord(obj) && Object.hasOwn(obj, String(segment)) ? obj[String(segment)] : void 0;
180
+ };
181
+ var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
182
+ var slotAtPath = (obj, path) => {
183
+ const walked = parsePath(path).reduce(
184
+ (acc, segment) => {
185
+ const container = acc.current;
186
+ const addressable = typeof segment === "number" ? Array.isArray(container) ? segment < container.length : isPlainRecord(container) : isPlainRecord(container);
187
+ return !acc.ok || !addressable ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
188
+ },
189
+ { ok: true, current: obj }
190
+ );
191
+ return walked.ok ? { exists: true, value: walked.current } : { exists: false };
192
+ };
193
+ var hasOwnSlot = (obj, path) => parsePath(path).reduce(
194
+ (acc, segment) => {
195
+ const container = acc.current;
196
+ const owns = Array.isArray(container) ? typeof segment === "number" && segment < container.length : isPlainRecord(container) && Object.hasOwn(container, String(segment));
197
+ return !acc.ok || !owns ? { ok: false, current: void 0 } : { ok: true, current: readSegment(container, segment) };
198
+ },
199
+ { ok: true, current: obj }
200
+ ).ok;
201
+ var writeIntoArray = (arr, index, rest, value, kindAt, prefix) => {
202
+ const length = Math.max(arr.length, index + 1);
203
+ return Array.from(
204
+ { length },
205
+ (_, i) => i === index ? writeSegments(arr[i], rest, value, kindAt, [...prefix, index]) : arr[i]
206
+ );
207
+ };
208
+ var refusesHugeIndex = (index) => {
209
+ if (index > MAX_ARRAY_INDEX) {
210
+ console.warn(
211
+ `[formstand] refusing to write array index ${index} (max ${MAX_ARRAY_INDEX}); value left unchanged.`
212
+ );
213
+ return true;
214
+ }
215
+ return false;
216
+ };
217
+ var writeSegments = (obj, segments, value, kindAt, prefix) => {
218
+ const [head, ...rest] = segments;
219
+ if (head === void 0) return value;
220
+ if (Array.isArray(obj)) {
221
+ if (typeof head !== "number") {
222
+ console.warn(
223
+ `[formstand] cannot write string key "${head}" into an array; value left unchanged.`
224
+ );
225
+ return obj;
226
+ }
227
+ if (refusesHugeIndex(head)) return obj;
228
+ return writeIntoArray(obj, head, rest, value, kindAt, prefix);
229
+ }
230
+ if (isPlainRecord(obj)) {
231
+ if (!isPlainObject(obj)) {
232
+ console.warn(
233
+ `[formstand] cannot write key "${String(head)}" through a non-plain object (Date/Map/Set/class instance); value left unchanged.`
234
+ );
235
+ return obj;
236
+ }
237
+ const key2 = String(head);
238
+ return {
239
+ ...obj,
240
+ [key2]: writeSegments(obj[key2], rest, value, kindAt, [...prefix, key2])
241
+ };
242
+ }
243
+ const kind = kindAt?.(prefix.map(String).join(".")) ?? (typeof head === "number" ? "array" : "record");
244
+ if (kind === "array") {
245
+ if (typeof head !== "number") {
246
+ console.warn(
247
+ `[formstand] cannot write string key "${head}" into an array; value left unchanged.`
248
+ );
249
+ return obj;
250
+ }
251
+ if (refusesHugeIndex(head)) return obj;
252
+ return writeIntoArray([], head, rest, value, kindAt, prefix);
253
+ }
254
+ const key = String(head);
255
+ return { [key]: writeSegments(void 0, rest, value, kindAt, [...prefix, key]) };
256
+ };
257
+ var setAtPath = (obj, path, value, containerKindAt2) => writeSegments(obj, parsePath(path), value, containerKindAt2, []);
106
258
  var issuePathToFormPath = (path) => path.filter((segment) => typeof segment !== "symbol").join(".");
107
259
  var expandIssue = (issue, prefix) => {
108
260
  const at = [...prefix, ...issue.path];
@@ -134,6 +286,12 @@ var validateAsync = async (schema, values) => {
134
286
  return result.success ? { kind: "valid", data: result.data } : { kind: "invalid", errors: flattenIssues(result.error.issues) };
135
287
  };
136
288
  var isAsyncRequiredError = (e) => e instanceof z.core.$ZodAsyncError || e instanceof Error && /Encountered Promise during synchronous parse/i.test(e.message);
289
+ var reportBackgroundFailure = (error) => {
290
+ console.error(
291
+ "[formstand] a background validation pass threw (an async refinement rejected). Its in-flight flags were released. The error:",
292
+ error
293
+ );
294
+ };
137
295
  var isPathOrChild = (key, path) => key === path || key.startsWith(`${path}.`);
138
296
  var prefixErrorKeys = (errors, path) => path === "" ? errors : Object.fromEntries(
139
297
  Object.entries(errors).map(([k, v]) => [
@@ -185,6 +343,42 @@ var schemaHasPath = (schema, path) => {
185
343
  };
186
344
  return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
187
345
  };
346
+ var schemaAllowsKindChangeAt = (schema, path) => {
347
+ const flexible = (s) => s instanceof z.ZodUnion || s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodPipe || s instanceof z.ZodAny || s instanceof z.ZodUnknown || s instanceof z.ZodLazy || s instanceof z.ZodCatch || s.def.coerce === true;
348
+ const strip = (s) => {
349
+ if (s instanceof z.ZodOptional || s instanceof z.ZodNullable) {
350
+ return strip(s.unwrap());
351
+ }
352
+ if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
353
+ return strip(s.def.innerType);
354
+ }
355
+ return s;
356
+ };
357
+ const walk = (s, segments, budget) => {
358
+ if (budget <= 0) return true;
359
+ const node = strip(s);
360
+ if (flexible(node)) return true;
361
+ const [head, ...rest] = segments;
362
+ if (head === void 0) return false;
363
+ if (node instanceof z.ZodObject) {
364
+ const shape = node.shape;
365
+ const child = Object.hasOwn(shape, String(head)) ? shape[String(head)] : void 0;
366
+ return child === void 0 ? true : walk(child, rest, budget - 1);
367
+ }
368
+ if (node instanceof z.ZodArray) {
369
+ return typeof head === "number" ? walk(node.element, rest, budget - 1) : true;
370
+ }
371
+ if (node instanceof z.ZodTuple && typeof head === "number") {
372
+ const item = node.def.items[head];
373
+ return item === void 0 ? true : walk(item, rest, budget - 1);
374
+ }
375
+ if (node instanceof z.ZodRecord) {
376
+ return walk(node.valueType, rest, budget - 1);
377
+ }
378
+ return true;
379
+ };
380
+ return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
381
+ };
188
382
  var hasChecks = (s) => {
189
383
  const checks = s.def.checks;
190
384
  return checks !== void 0 && checks.length > 0;
@@ -269,7 +463,7 @@ var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segme
269
463
  }, schema);
270
464
 
271
465
  // src/core/createForm.ts
272
- var shallowFieldEqual = (a, b) => Object.is(a.value, b.value) && a.error === b.error && a.touched === b.touched && a.dirty === b.dirty && a.isValidating === b.isValidating;
466
+ var shallowFieldEqual = (a, b) => Object.is(a.value, b.value) && Object.is(a.initialValue, b.initialValue) && a.error === b.error && a.touched === b.touched && a.dirty === b.dirty && a.isValidating === b.isValidating;
273
467
  var emptyErrors = {};
274
468
  var emptyBools = {};
275
469
  var identityMapper = (n) => n;
@@ -327,7 +521,26 @@ var reuseErrorRefs = (next, prev) => {
327
521
  const sameShape = entries.length === Object.keys(prev).length && entries.every(([k, v]) => entryAt(prev, k) === v);
328
522
  return sameShape ? prev : Object.fromEntries(entries);
329
523
  };
330
- var scopeSettledResult = (result, path, viaSubschema) => result.kind !== "invalid" ? emptyErrors : viaSubschema ? prefixErrorKeys(result.errors, path) : path === "" ? result.errors : pickScope(result.errors, [path]);
524
+ var noScopeErrors = { scoped: emptyErrors, full: null };
525
+ var scopeParse = (result, path, viaSubschema) => {
526
+ const errors = result.kind === "invalid" ? result.errors : emptyErrors;
527
+ return viaSubschema ? { scoped: prefixErrorKeys(errors, path), full: null } : {
528
+ scoped: path === "" ? errors : pickScope(errors, [path]),
529
+ full: errors
530
+ };
531
+ };
532
+ var pruneUnreported = (prev, full) => {
533
+ const entries = Object.entries(prev);
534
+ const kept = entries.filter(([k]) => Object.hasOwn(full, k));
535
+ return kept.length === entries.length ? prev : Object.fromEntries(kept);
536
+ };
537
+ var dropEmptyLists = (map) => {
538
+ const entries = Object.entries(map);
539
+ const kept = entries.filter(
540
+ ([, list]) => list !== void 0 && list.length > 0
541
+ );
542
+ return kept.length === entries.length ? map : Object.fromEntries(kept);
543
+ };
331
544
  var mergeErrorChannels = (schema, server) => {
332
545
  const extra = Object.entries(server).filter(
333
546
  ([k]) => !Object.hasOwn(schema, k)
@@ -437,17 +650,17 @@ var createForm = (schema, options) => {
437
650
  const merged = scopes.reduce(
438
651
  (acc, { path, scope }) => scope.kind === "skip" ? acc : {
439
652
  ...acc,
440
- ...scopeSettledResult(
653
+ ...scopeParse(
441
654
  validateSyncLatched(path, scope.schema, scope.value),
442
655
  path,
443
656
  true
444
- )
657
+ ).scoped
445
658
  },
446
659
  {}
447
660
  );
448
661
  store.setState((state) => ({
449
662
  ...state,
450
- ...commitScopedErrors(state, merged, paths)
663
+ ...commitScopedErrors(state, merged, paths, null)
451
664
  }));
452
665
  return settledFields(merged);
453
666
  }
@@ -456,7 +669,7 @@ var createForm = (schema, options) => {
456
669
  const requestedErrors = pickScope(fullErrors, paths);
457
670
  store.setState((state) => ({
458
671
  ...state,
459
- ...commitScopedErrors(state, requestedErrors, paths)
672
+ ...commitScopedErrors(state, requestedErrors, paths, fullErrors)
460
673
  }));
461
674
  return settledFields(requestedErrors);
462
675
  } catch (e) {
@@ -501,7 +714,7 @@ var createForm = (schema, options) => {
501
714
  const slot = slotAtPath(values, path);
502
715
  return slot.exists ? { kind: "parse", schema: sub, value: slot.value, viaSubschema: true } : { kind: "skip" };
503
716
  };
504
- const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? emptyErrors : scopeSettledResult(
717
+ const parseScopeAsync = async (path, scope) => scope.kind === "skip" ? noScopeErrors : scopeParse(
505
718
  await validateAsync(scope.schema, scope.value),
506
719
  path,
507
720
  scope.viaSubschema
@@ -521,14 +734,18 @@ var createForm = (schema, options) => {
521
734
  };
522
735
  const settledFields = (scoped) => Object.keys(scoped).length === 0 ? { kind: "valid" } : { kind: "invalid", errors: scoped };
523
736
  const settledFormResult = (result) => settledFields(result.kind === "invalid" ? result.errors : emptyErrors);
524
- const commitScopedErrors = (state, scoped, paths) => errorChannels(state, {
737
+ const commitScopedErrors = (state, scoped, paths, full) => errorChannels(state, {
525
738
  schemaErrors: reuseErrorRefs(
526
- mergeScopedErrors(state.schemaErrors, scoped, paths),
739
+ mergeScopedErrors(
740
+ full === null ? state.schemaErrors : pruneUnreported(state.schemaErrors, full),
741
+ scoped,
742
+ paths
743
+ ),
527
744
  state.schemaErrors
528
745
  ),
529
746
  serverErrors: omitScope(state.serverErrors, paths)
530
747
  });
531
- const commitFieldErrors = (state, scoped, path) => path === "" ? commitFullPass(state, scoped) : commitScopedErrors(state, scoped, [path]);
748
+ const commitFieldErrors = (state, parse, path) => path === "" ? commitFullPass(state, parse.scoped) : commitScopedErrors(state, parse.scoped, [path], parse.full);
532
749
  const validateField = (path) => {
533
750
  warnMissingSchemaPath("validateField", path);
534
751
  const scope = fieldScopeFor(path, store.getState().values);
@@ -536,7 +753,7 @@ var createForm = (schema, options) => {
536
753
  return { kind: "pending", promise: validateFieldAsync(path) };
537
754
  }
538
755
  try {
539
- const scoped = scope.kind === "skip" ? emptyErrors : scopeSettledResult(
756
+ const parse = scope.kind === "skip" ? noScopeErrors : scopeParse(
540
757
  validateSyncLatched(
541
758
  scope.viaSubschema ? path : "",
542
759
  scope.schema,
@@ -547,9 +764,9 @@ var createForm = (schema, options) => {
547
764
  );
548
765
  store.setState((state) => ({
549
766
  ...state,
550
- ...commitFieldErrors(state, scoped, path)
767
+ ...commitFieldErrors(state, parse, path)
551
768
  }));
552
- return fieldResult(scoped);
769
+ return fieldResult(parse.scoped);
553
770
  } catch (e) {
554
771
  if (isAsyncRequiredError(e)) {
555
772
  return { kind: "pending", promise: validateFieldAsync(path) };
@@ -559,11 +776,15 @@ var createForm = (schema, options) => {
559
776
  };
560
777
  const snapshotField = (state, path) => {
561
778
  const value = getAtPath(state.values, path);
779
+ const initialValue = getAtPath(state.initialValues, path);
780
+ const error = entryAt(state.errors, path);
562
781
  return {
563
782
  value,
564
- error: entryAt(state.errors, path),
783
+ initialValue,
784
+ error,
785
+ firstError: error?.[0],
565
786
  touched: flagAt(state.touched, path),
566
- dirty: isFieldDirty(value, getAtPath(state.initialValues, path)),
787
+ dirty: isFieldDirty(value, initialValue),
567
788
  isValidating: flagAt(state.isValidating, path)
568
789
  };
569
790
  };
@@ -577,7 +798,14 @@ var createForm = (schema, options) => {
577
798
  const pass = claimPass(FORM_SEQ_KEY);
578
799
  const valuesAtStart = store.getState().values;
579
800
  store.setState((state) => ({ ...state, isValidatingForm: true }));
580
- const result = await validateAsync(schema, valuesAtStart);
801
+ const result = await validateAsync(schema, valuesAtStart).catch(
802
+ (error) => {
803
+ if (sequences.get(FORM_SEQ_KEY) === pass) {
804
+ store.setState((state) => ({ ...state, isValidatingForm: false }));
805
+ }
806
+ throw error;
807
+ }
808
+ );
581
809
  const live = store.getState();
582
810
  const stillOwns = sequences.get(FORM_SEQ_KEY) === pass;
583
811
  if (!stillOwns || live.values !== valuesAtStart) {
@@ -617,12 +845,15 @@ var createForm = (schema, options) => {
617
845
  ...Object.fromEntries(paths.map((p) => [p, true]))
618
846
  }
619
847
  }));
620
- const parsed = allViaSubschema ? {
848
+ const parseBatch = async () => allViaSubschema ? {
621
849
  kind: "slots",
622
850
  byPath: new Map(
623
851
  await Promise.all(
624
852
  scopes.map(
625
- async ({ path, scope }) => [path, await parseScopeAsync(path, scope)]
853
+ async ({ path, scope }) => [
854
+ path,
855
+ (await parseScopeAsync(path, scope)).scoped
856
+ ]
626
857
  )
627
858
  )
628
859
  )
@@ -632,6 +863,18 @@ var createForm = (schema, options) => {
632
863
  (result) => result.kind === "invalid" ? result.errors : emptyErrors
633
864
  )
634
865
  };
866
+ const parsed = await parseBatch().catch((error) => {
867
+ const stillOwned = new Set(
868
+ paths.filter((p) => sequences.get(p) === passes.get(p))
869
+ );
870
+ if (stillOwned.size > 0) {
871
+ store.setState((state) => ({
872
+ ...state,
873
+ isValidating: omitKeys(state.isValidating, stillOwned)
874
+ }));
875
+ }
876
+ throw error;
877
+ });
635
878
  const errorsFor = (path) => {
636
879
  switch (parsed.kind) {
637
880
  case "slots":
@@ -660,7 +903,8 @@ var createForm = (schema, options) => {
660
903
  Object.fromEntries(
661
904
  fresh.flatMap((path) => Object.entries(errorsFor(path)))
662
905
  ),
663
- fresh
906
+ fresh,
907
+ parsed.kind === "full" ? parsed.errors : null
664
908
  ),
665
909
  isValidating: omitKeys(state.isValidating, ownedKeys)
666
910
  }));
@@ -681,24 +925,32 @@ var createForm = (schema, options) => {
681
925
  ...state,
682
926
  isValidating: { ...state.isValidating, [path]: true }
683
927
  }));
684
- const scoped = await parseScopeAsync(path, scope);
685
- const live = store.getState();
686
- const stillOwns = sequences.get(path) === pass;
687
- if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
688
- if (stillOwns) {
928
+ const releaseOwnFlag = () => {
929
+ if (sequences.get(path) === pass) {
689
930
  store.setState((state) => ({
690
931
  ...state,
691
932
  isValidating: omitKey(state.isValidating, path)
692
933
  }));
693
934
  }
694
- return fieldResult(scoped);
935
+ };
936
+ const parse = await parseScopeAsync(path, scope).catch(
937
+ (error) => {
938
+ releaseOwnFlag();
939
+ throw error;
940
+ }
941
+ );
942
+ const live = store.getState();
943
+ const stillOwns = sequences.get(path) === pass;
944
+ if (!stillOwns || scopeStale(scope, path, valuesAtStart, live.values)) {
945
+ releaseOwnFlag();
946
+ return fieldResult(parse.scoped);
695
947
  }
696
948
  store.setState((state) => ({
697
949
  ...state,
698
- ...commitFieldErrors(state, scoped, path),
950
+ ...commitFieldErrors(state, parse, path),
699
951
  isValidating: omitKey(state.isValidating, path)
700
952
  }));
701
- return fieldResult(scoped);
953
+ return fieldResult(parse.scoped);
702
954
  };
703
955
  const applyArrayOp = (path, nextArray, mapper, indexInBounds) => {
704
956
  const current = getAtPath(store.getState().values, path);
@@ -757,7 +1009,9 @@ var createForm = (schema, options) => {
757
1009
  count: 0,
758
1010
  baseline: false
759
1011
  };
760
- const submit = async (onValid, onInvalid, submitOptions) => {
1012
+ const submit = async (onValid, onInvalidOrOptions, positionalOptions) => {
1013
+ const submitOptions = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
1014
+ const onInvalid = typeof onInvalidOrOptions === "function" ? onInvalidOrOptions : submitOptions?.onInvalid;
761
1015
  if (inFlight.count > 0 && submitOptions?.force !== true) {
762
1016
  return { kind: "skipped" };
763
1017
  }
@@ -798,6 +1052,7 @@ var createForm = (schema, options) => {
798
1052
  try {
799
1053
  onInvalid?.(merged);
800
1054
  } catch (error) {
1055
+ submitOptions?.onError?.(error);
801
1056
  return { kind: "error", error };
802
1057
  }
803
1058
  return { kind: "invalid", errors: merged };
@@ -824,7 +1079,10 @@ var createForm = (schema, options) => {
824
1079
  }
825
1080
  };
826
1081
  if (options.validateOnMount === true) {
827
- validate();
1082
+ const result = validate();
1083
+ if (result.kind === "pending") {
1084
+ void result.promise.catch(reportBackgroundFailure);
1085
+ }
828
1086
  }
829
1087
  return Object.freeze({
830
1088
  schema,
@@ -917,7 +1175,7 @@ var createForm = (schema, options) => {
917
1175
  },
918
1176
  setErrors: (errors) => store.setState((state) => ({
919
1177
  ...state,
920
- ...errorChannels(state, { serverErrors: errors })
1178
+ ...errorChannels(state, { serverErrors: dropEmptyLists(errors) })
921
1179
  })),
922
1180
  addErrors: (errors) => store.setState((state) => {
923
1181
  const entries = Object.entries(errors);
@@ -960,11 +1218,11 @@ var createForm = (schema, options) => {
960
1218
  if (next.values !== state.values) {
961
1219
  clearArrayOpsUnder(store, "");
962
1220
  }
963
- const serverErrors = patch.serverErrors !== void 0 ? patch.serverErrors : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
1221
+ const serverErrors = patch.serverErrors !== void 0 ? dropEmptyLists(patch.serverErrors) : next.values !== state.values ? releaseChangedSlices(state.serverErrors, state.values, next.values) : state.serverErrors;
964
1222
  store.setState(() => ({
965
1223
  ...next,
966
1224
  ...errorChannels(state, {
967
- schemaErrors: next.schemaErrors,
1225
+ schemaErrors: patch.schemaErrors === void 0 ? state.schemaErrors : dropEmptyLists(patch.schemaErrors),
968
1226
  serverErrors
969
1227
  })
970
1228
  }));
@@ -1073,9 +1331,14 @@ var createForm = (schema, options) => {
1073
1331
  swapIndices(a, b),
1074
1332
  (len) => Number.isInteger(a) && Number.isInteger(b) && a >= 0 && a < len && b >= 0 && b < len
1075
1333
  ),
1076
- handleSubmit: (onValid, onInvalid, opts) => async (event) => {
1334
+ handleSubmit: (onValid, onInvalidOrOptions, positionalOptions) => async (event) => {
1077
1335
  event?.preventDefault();
1078
- const result = await submit(onValid, onInvalid, opts);
1336
+ const opts = typeof onInvalidOrOptions === "object" ? onInvalidOrOptions : positionalOptions;
1337
+ const result = await submit(
1338
+ onValid,
1339
+ onInvalidOrOptions,
1340
+ positionalOptions
1341
+ );
1079
1342
  if (result.kind === "error" && opts?.onError === void 0 && isDevBuild()) {
1080
1343
  console.warn(
1081
1344
  "[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
@@ -1101,8 +1364,8 @@ var createForm = (schema, options) => {
1101
1364
  restore: (snap) => {
1102
1365
  clearArrayOpsUnder(store, "");
1103
1366
  store.setState(() => {
1104
- const schemaErrors = snap.schemaErrors ?? emptyErrors;
1105
- const serverErrors = snap.serverErrors ?? emptyErrors;
1367
+ const schemaErrors = dropEmptyLists(snap.schemaErrors ?? emptyErrors);
1368
+ const serverErrors = dropEmptyLists(snap.serverErrors ?? emptyErrors);
1106
1369
  return {
1107
1370
  ...snap,
1108
1371
  schemaErrors,
@@ -1132,11 +1395,13 @@ var createDebouncer = (ms) => {
1132
1395
  const timer = {
1133
1396
  current: null
1134
1397
  };
1398
+ const scheduled = { current: null };
1135
1399
  const cancel = () => {
1136
1400
  if (timer.current !== null) {
1137
1401
  clearTimeout(timer.current);
1138
1402
  timer.current = null;
1139
1403
  }
1404
+ scheduled.current = null;
1140
1405
  };
1141
1406
  const schedule = (run) => {
1142
1407
  cancel();
@@ -1144,37 +1409,50 @@ var createDebouncer = (ms) => {
1144
1409
  run();
1145
1410
  return;
1146
1411
  }
1412
+ scheduled.current = run;
1147
1413
  timer.current = setTimeout(() => {
1148
1414
  timer.current = null;
1415
+ scheduled.current = null;
1149
1416
  run();
1150
1417
  }, ms);
1151
1418
  };
1419
+ const flush = () => {
1420
+ const run = scheduled.current;
1421
+ if (run === null) return;
1422
+ cancel();
1423
+ run();
1424
+ };
1152
1425
  return Object.freeze({
1153
1426
  schedule,
1154
1427
  cancel,
1428
+ flush,
1155
1429
  pending: () => timer.current !== null
1156
1430
  });
1157
1431
  };
1158
1432
 
1159
1433
  // src/core/persist.ts
1160
1434
  var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1161
- var conflictsWith = (draft, reference) => {
1162
- if (draft === void 0 || draft === null) return false;
1163
- if (reference === void 0 || reference === null) return false;
1435
+ var joinPath = (base, key) => base === "" ? String(key) : `${base}.${String(key)}`;
1436
+ var conflictPath = (draft, reference, path, flexibleAt) => {
1437
+ if (draft === void 0 || draft === null) return null;
1438
+ if (reference === void 0 || reference === null) return null;
1164
1439
  const draftKind = kindOf(draft);
1165
1440
  const referenceKind = kindOf(reference);
1166
- if (draftKind !== referenceKind) return true;
1441
+ if (draftKind !== referenceKind) return flexibleAt(path) ? null : path;
1167
1442
  if (draftKind === "array") {
1168
1443
  const [d] = draft;
1169
1444
  const [r] = reference;
1170
- return conflictsWith(d, r);
1445
+ return conflictPath(d, r, joinPath(path, 0), flexibleAt);
1171
1446
  }
1172
1447
  if (draftKind === "object") {
1173
1448
  const d = draft;
1174
1449
  const r = reference;
1175
- return Object.keys(d).some((key) => conflictsWith(d[key], r[key]));
1450
+ return Object.keys(d).reduce(
1451
+ (found, key) => found ?? conflictPath(d[key], r[key], joinPath(path, key), flexibleAt),
1452
+ null
1453
+ );
1176
1454
  }
1177
- return false;
1455
+ return null;
1178
1456
  };
1179
1457
  var defaultStorage = () => {
1180
1458
  try {
@@ -1187,7 +1465,35 @@ var persistForm = (form, options) => {
1187
1465
  const storage = options.storage ?? defaultStorage();
1188
1466
  const debounceMs = options.debounceMs ?? 300;
1189
1467
  const apply = options.apply ?? "adopt";
1190
- const applyDraft = (values) => apply === "edit" || apply === "restore" ? form.setValues(values) : form.adoptValues(values);
1468
+ const schema = form.schema;
1469
+ const fillRequiredGaps = (draft, initial, path) => {
1470
+ if (!isPlainObject(draft) || !isPlainObject(initial)) return draft;
1471
+ return Object.keys(initial).reduce(
1472
+ (acc, key) => {
1473
+ const childPath = joinPath(path, key);
1474
+ if (Object.hasOwn(draft, key)) {
1475
+ const next = fillRequiredGaps(draft[key], initial[key], childPath);
1476
+ return Object.is(next, draft[key]) ? acc : { ...acc, [key]: next };
1477
+ }
1478
+ if (initial[key] === void 0) return acc;
1479
+ const sub = fieldSchemaForIntrospection(schema, childPath);
1480
+ return sub !== null && isClearableSchema(sub) ? acc : { ...acc, [key]: initial[key] };
1481
+ },
1482
+ draft
1483
+ );
1484
+ };
1485
+ const applyDraft = (values) => {
1486
+ const filled = fillRequiredGaps(
1487
+ values,
1488
+ form.getState().initialValues,
1489
+ ""
1490
+ );
1491
+ if (apply === "edit" || apply === "restore") {
1492
+ form.setValues(filled);
1493
+ } else {
1494
+ form.adoptValues(filled);
1495
+ }
1496
+ };
1191
1497
  const referenceShape = () => {
1192
1498
  try {
1193
1499
  return JSON.parse(JSON.stringify(form.getState().initialValues));
@@ -1195,30 +1501,67 @@ var persistForm = (form, options) => {
1195
1501
  return void 0;
1196
1502
  }
1197
1503
  };
1198
- const load = () => {
1504
+ const flexibleAt = (path) => schemaAllowsKindChangeAt(schema, path);
1505
+ const inspect = () => {
1506
+ const raw = (() => {
1507
+ try {
1508
+ return storage === null ? null : storage.getItem(options.key);
1509
+ } catch {
1510
+ return null;
1511
+ }
1512
+ })();
1513
+ if (raw === null) return { kind: "absent" };
1199
1514
  try {
1200
- const raw = storage === null ? null : storage.getItem(options.key);
1201
- if (raw === null) return false;
1202
1515
  const parsed = JSON.parse(raw);
1203
1516
  const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
1204
- if (options.version !== void 0 && !versioned) return false;
1517
+ if (options.version !== void 0 && !versioned) {
1518
+ return {
1519
+ kind: "ignored",
1520
+ reason: `it carries no version and this configuration expects version ${String(options.version)}`
1521
+ };
1522
+ }
1205
1523
  if (options.version === void 0 && typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "__v") && Object.hasOwn(parsed, "values")) {
1206
- return false;
1524
+ return {
1525
+ kind: "ignored",
1526
+ reason: "it was written with a version and this configuration has none"
1527
+ };
1207
1528
  }
1208
1529
  const wrapper = parsed;
1209
- if (versioned && wrapper.__v !== options.version) return false;
1210
- if (versioned && wrapper.values === void 0) return false;
1530
+ if (versioned && wrapper.__v !== options.version) {
1531
+ return {
1532
+ kind: "ignored",
1533
+ reason: `its version (${String(wrapper.__v)}) is not the configured version (${String(options.version)})`
1534
+ };
1535
+ }
1536
+ if (versioned && wrapper.values === void 0) {
1537
+ return { kind: "ignored", reason: "its version wrapper holds no values" };
1538
+ }
1211
1539
  const values = versioned ? wrapper.values : parsed;
1212
- if (values === null || values === void 0) return false;
1540
+ if (values === null || values === void 0) {
1541
+ return { kind: "ignored", reason: "it holds no values" };
1542
+ }
1213
1543
  const reference = referenceShape();
1214
- if (reference !== void 0 && conflictsWith(values, reference)) {
1215
- return false;
1544
+ const conflict = reference === void 0 ? null : conflictPath(values, reference, "", flexibleAt);
1545
+ if (conflict !== null) {
1546
+ return {
1547
+ kind: "ignored",
1548
+ reason: `its shape conflicts with the form's initial values at "${conflict}" (set the version option when the schema changes)`
1549
+ };
1216
1550
  }
1217
1551
  applyDraft(values);
1218
- return true;
1552
+ return { kind: "applied" };
1219
1553
  } catch {
1220
- return false;
1554
+ return { kind: "ignored", reason: "it is not valid JSON" };
1555
+ }
1556
+ };
1557
+ const load = () => {
1558
+ const outcome = inspect();
1559
+ if (outcome.kind === "ignored" && isDevBuild()) {
1560
+ console.warn(
1561
+ `[formstand] persistForm: ignored the draft under "${options.key}": ${outcome.reason}. The next edit overwrites it.`
1562
+ );
1221
1563
  }
1564
+ return outcome.kind === "applied";
1222
1565
  };
1223
1566
  const writeDraft = (values) => {
1224
1567
  try {
@@ -1232,24 +1575,28 @@ var persistForm = (form, options) => {
1232
1575
  }
1233
1576
  };
1234
1577
  const writes = createDebouncer(debounceMs);
1235
- const cancelPending = () => writes.cancel();
1236
1578
  if (apply !== "manual") {
1237
1579
  load();
1238
1580
  }
1581
+ const cleared = { current: false };
1239
1582
  const unsubscribe = form.watchValues((values) => {
1583
+ if (cleared.current && values === form.getState().initialValues) return;
1584
+ cleared.current = false;
1240
1585
  writes.schedule(() => writeDraft(values));
1241
1586
  });
1242
1587
  return Object.freeze({
1243
1588
  load,
1589
+ flush: () => writes.flush(),
1244
1590
  clear: () => {
1245
- cancelPending();
1591
+ writes.cancel();
1592
+ cleared.current = true;
1246
1593
  try {
1247
1594
  storage?.removeItem(options.key);
1248
1595
  } catch {
1249
1596
  }
1250
1597
  },
1251
1598
  dispose: () => {
1252
- cancelPending();
1599
+ writes.flush();
1253
1600
  unsubscribe();
1254
1601
  }
1255
1602
  });
@@ -1261,6 +1608,7 @@ var usePersistForm = (form, options) => {
1261
1608
  latestOptions.current = options;
1262
1609
  });
1263
1610
  const key = options.key;
1611
+ const storage = options.storage;
1264
1612
  useEffect(() => {
1265
1613
  const handle = persistForm(form, latestOptions.current);
1266
1614
  inner.current = handle;
@@ -1268,10 +1616,11 @@ var usePersistForm = (form, options) => {
1268
1616
  inner.current = null;
1269
1617
  handle.dispose();
1270
1618
  };
1271
- }, [form, key]);
1619
+ }, [form, key, storage]);
1272
1620
  return useMemo(
1273
1621
  () => ({
1274
1622
  load: () => inner.current?.load() ?? false,
1623
+ flush: () => inner.current?.flush(),
1275
1624
  clear: () => inner.current?.clear(),
1276
1625
  // dispose stops THIS component's watching early; the effect cleanup
1277
1626
  // still runs its own dispose harmlessly (persistForm's dispose is
@@ -1305,9 +1654,12 @@ var useForm = (schema, options) => {
1305
1654
  var hasFieldError = (error) => error !== void 0 && error.length > 0;
1306
1655
  var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
1307
1656
  var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
1657
+ var DECIMAL_TEXT = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
1308
1658
  var parseNumberText = (text) => {
1309
- if (text.trim() === "") return { kind: "empty" };
1310
- const parsed = Number(text);
1659
+ const trimmed = text.trim();
1660
+ if (trimmed === "") return { kind: "empty" };
1661
+ if (!DECIMAL_TEXT.test(trimmed)) return { kind: "invalid" };
1662
+ const parsed = Number(trimmed);
1311
1663
  return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
1312
1664
  };
1313
1665
  var textInputProps = (field) => ({
@@ -1335,11 +1687,11 @@ var numberInputProps = (field) => ({
1335
1687
  onBlur: field.onBlur
1336
1688
  });
1337
1689
  var dateToInputText = (value) => {
1338
- if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
1339
- return "";
1340
- }
1690
+ if (value === void 0 || value === null) return "";
1691
+ const date = value instanceof Date ? value : new Date(value);
1692
+ if (Number.isNaN(date.getTime())) return "";
1341
1693
  const pad = (part) => String(part).padStart(2, "0");
1342
- return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
1694
+ return `${String(date.getFullYear()).padStart(4, "0")}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
1343
1695
  };
1344
1696
  var parseDateText = (text) => {
1345
1697
  if (text.trim() === "") return { kind: "empty" };
@@ -1419,7 +1771,8 @@ var useMaskedInput = (field, options) => {
1419
1771
  field.setValue(parsed.value);
1420
1772
  return;
1421
1773
  case "empty": {
1422
- const empty = field.clearable || typeof field.initialValue !== "string" ? field.emptyValue : "";
1774
+ const isStringField = typeof field.initialValue === "string" || typeof field.value === "string";
1775
+ const empty = field.clearable || !isStringField ? field.emptyValue : "";
1423
1776
  setEdit({ raw: text, pushed: empty });
1424
1777
  field.setValue(empty);
1425
1778
  return;
@@ -1478,7 +1831,8 @@ var useFormSteps = (form, steps) => {
1478
1831
  const goTo = useCallback(
1479
1832
  async (target) => {
1480
1833
  const count = latest.current.length;
1481
- const clamped = Math.max(0, Math.min(target, count - 1));
1834
+ if (!Number.isFinite(target)) return currentStep.current;
1835
+ const clamped = Math.max(0, Math.min(Math.trunc(target), count - 1));
1482
1836
  if (clamped <= step) {
1483
1837
  setStep(clamped);
1484
1838
  return clamped;
@@ -1551,8 +1905,7 @@ var useFormAction = (form, onValid, options) => {
1551
1905
  const { onValid: handler, options: opts } = latest.current;
1552
1906
  const result = await form.submit(
1553
1907
  (data) => handler(data, formData),
1554
- opts?.onInvalid,
1555
- opts?.onError === void 0 ? void 0 : { onError: opts.onError }
1908
+ opts ?? {}
1556
1909
  );
1557
1910
  warnUnobserved(result, opts, "useFormAction");
1558
1911
  },
@@ -1568,8 +1921,7 @@ var useFormActionState = (form, action, initialState, options) => {
1568
1921
  async (data) => {
1569
1922
  nextRef.current = await action(prev, data, formData);
1570
1923
  },
1571
- options?.onInvalid,
1572
- options?.onError === void 0 ? void 0 : { onError: options.onError }
1924
+ options ?? {}
1573
1925
  );
1574
1926
  warnUnobserved(result, options, "useFormActionState");
1575
1927
  return nextRef.current;
@@ -1635,6 +1987,12 @@ function useField(form, pathArg, options) {
1635
1987
  debouncerRef.current = null;
1636
1988
  };
1637
1989
  }, [form, path, debounceMs]);
1990
+ const validateNow = useCallback(() => {
1991
+ const result = form.validateField(path);
1992
+ if (result.kind === "pending") {
1993
+ void result.promise.catch(reportBackgroundFailure);
1994
+ }
1995
+ }, [form, path]);
1638
1996
  const triggerValidate = useCallback(() => {
1639
1997
  if (debounceMs !== void 0 && debounceMs > 0) {
1640
1998
  const debouncer = debouncerRef.current ?? createDebouncer(debounceMs);
@@ -1646,12 +2004,12 @@ function useField(form, pathArg, options) {
1646
2004
  const rebased = state.values !== scheduledValues && state.values === state.initialValues;
1647
2005
  if (rebased) return;
1648
2006
  if (!Object.is(getAtPath(state.values, path), scheduled)) return;
1649
- void form.validateFieldAsync(path);
2007
+ void form.validateFieldAsync(path).catch(reportBackgroundFailure);
1650
2008
  });
1651
2009
  return;
1652
2010
  }
1653
- form.validateField(path);
1654
- }, [form, path, debounceMs]);
2011
+ validateNow();
2012
+ }, [form, path, debounceMs, validateNow]);
1655
2013
  const setValue = useCallback(
1656
2014
  (value) => {
1657
2015
  const before = form.store.getState();
@@ -1700,9 +2058,10 @@ function useField(form, pathArg, options) {
1700
2058
  state.submitCount > 0,
1701
2059
  true
1702
2060
  )) {
1703
- triggerValidate();
2061
+ debouncerRef.current?.cancel();
2062
+ validateNow();
1704
2063
  }
1705
- }, [form, path, triggerValidate]);
2064
+ }, [form, path, validateNow]);
1706
2065
  return useMemo(
1707
2066
  () => ({
1708
2067
  path: slice.path,
@@ -2054,6 +2413,14 @@ function useFieldArray(form, pathArg) {
2054
2413
  ]
2055
2414
  );
2056
2415
  }
2416
+
2417
+ // src/react/useVariantFieldArray.ts
2418
+ function useVariantFieldArray(form, unionPath, field) {
2419
+ return useFieldArray(
2420
+ form,
2421
+ `${unionPath}.${field}`
2422
+ );
2423
+ }
2057
2424
  var useFormError = (form) => useStore(
2058
2425
  form.store,
2059
2426
  useShallow((state) => state.errors[""])
@@ -2061,29 +2428,37 @@ var useFormError = (form) => useStore(
2061
2428
  function useFormValues(form) {
2062
2429
  return useStore(form.store, (state) => state.values);
2063
2430
  }
2064
-
2065
- // src/react/useFields.ts
2066
2431
  function useFields(form, paths) {
2432
+ const [arity] = useState(paths.length);
2433
+ if (paths.length !== arity) {
2434
+ throw new Error(
2435
+ `[formstand] useFields: the paths tuple changed length (${String(arity)} to ${String(paths.length)}); its length and order must be stable across renders, so pass a literal or a module-level constant.`
2436
+ );
2437
+ }
2067
2438
  const fields = paths.map(
2068
2439
  (path) => (
2069
2440
  // eslint-disable-next-line react-hooks/rules-of-hooks -- a fixed hook count by the stable-arity contract above; each element must be a REAL UseFieldReturn so it spreads into the prop builders
2070
2441
  useField(form, path)
2071
2442
  )
2072
2443
  );
2073
- const merged = [...new Set(fields.flatMap((field) => field.error ?? []))];
2074
- return {
2075
- fields,
2076
- error: merged.length > 0 ? merged : void 0,
2077
- firstError: merged[0],
2078
- touched: fields.some((field) => field.touched),
2079
- dirty: fields.some((field) => field.dirty),
2080
- isValidating: fields.some((field) => field.isValidating),
2081
- onBlur: () => {
2082
- fields.forEach((field) => {
2083
- field.onBlur();
2084
- });
2085
- }
2086
- };
2444
+ return useMemo(() => {
2445
+ const merged = [
2446
+ ...new Set(fields.flatMap((field) => field.error ?? []))
2447
+ ];
2448
+ return {
2449
+ fields,
2450
+ error: merged.length > 0 ? merged : void 0,
2451
+ firstError: merged[0],
2452
+ touched: fields.some((field) => field.touched),
2453
+ dirty: fields.some((field) => field.dirty),
2454
+ isValidating: fields.some((field) => field.isValidating),
2455
+ onBlur: () => {
2456
+ fields.forEach((field) => {
2457
+ field.onBlur();
2458
+ });
2459
+ }
2460
+ };
2461
+ }, fields);
2087
2462
  }
2088
2463
  function useIsDirty(form, path) {
2089
2464
  return useStore(
@@ -2132,6 +2507,11 @@ var createFormHooks = (form, name) => {
2132
2507
  field
2133
2508
  );
2134
2509
  const useBoundFieldArray = (path) => useFieldArray(structural, path);
2510
+ const useBoundVariantFieldArray = (unionPath, field) => useVariantFieldArray(
2511
+ structural,
2512
+ unionPath,
2513
+ field
2514
+ );
2135
2515
  const useBoundSelector = (selector) => useFormSelector(structural, selector);
2136
2516
  const useBoundSelectorShallow = (selector) => useFormSelectorShallow(structural, selector);
2137
2517
  const useBoundError = () => useFormError(structural);
@@ -2146,11 +2526,15 @@ var createFormHooks = (form, name) => {
2146
2526
  );
2147
2527
  const useBoundSubmitCount = () => useSubmitCount(structural);
2148
2528
  const useBoundSteps = (steps) => useFormSteps(form, steps);
2529
+ const useBoundAction = (onValid, options) => useFormAction(form, onValid, options);
2530
+ const useBoundActionState = (action, initialState, options) => useFormActionState(form, action, initialState, options);
2531
+ const useBoundPersist = (options) => usePersistForm(form, options);
2149
2532
  const prefix = capitalize(name ?? "");
2150
2533
  return {
2151
2534
  [`use${prefix}Field`]: useBoundField,
2152
2535
  [`use${prefix}VariantField`]: useBoundVariantField,
2153
2536
  [`use${prefix}FieldArray`]: useBoundFieldArray,
2537
+ [`use${prefix}VariantFieldArray`]: useBoundVariantFieldArray,
2154
2538
  [`use${prefix}Selector`]: useBoundSelector,
2155
2539
  [`use${prefix}SelectorShallow`]: useBoundSelectorShallow,
2156
2540
  [`use${prefix}Error`]: useBoundError,
@@ -2161,7 +2545,10 @@ var createFormHooks = (form, name) => {
2161
2545
  [`use${prefix}SubmitCount`]: useBoundSubmitCount,
2162
2546
  [`use${prefix}Values`]: useBoundValues,
2163
2547
  [`use${prefix}Fields`]: useBoundFields,
2164
- [`use${prefix}Steps`]: useBoundSteps
2548
+ [`use${prefix}Steps`]: useBoundSteps,
2549
+ [`use${prefix}Action`]: useBoundAction,
2550
+ [`use${prefix}ActionState`]: useBoundActionState,
2551
+ [`use${prefix}Persist`]: useBoundPersist
2165
2552
  };
2166
2553
  };
2167
2554
  var useFieldA11y = (error) => {
@@ -2456,6 +2843,6 @@ var focusField = (path, root) => {
2456
2843
  return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(namedControls(scope).filter(isFocusCandidate)) : focusFirstOf(pathCandidates(scope, [path]).filter(isFocusCandidate));
2457
2844
  };
2458
2845
 
2459
- export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, persistForm, selectProps, textInputProps, useField, useFieldArray, useFields, useForm, useFormAction, useFormActionState, useFormError, useFormSteps, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useMaskedInput, useNumberInput, usePersistForm, useSubmitCount, useVariantField, validateAsync, validateSync };
2846
+ export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, getAtPath, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, parsePath, persistForm, selectProps, setAtPath, textInputProps, useField, useFieldArray, useFields, useForm, useFormAction, useFormActionState, useFormError, useFormSteps, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useMaskedInput, useNumberInput, usePersistForm, useSubmitCount, useVariantField, useVariantFieldArray, validateAsync, validateSync };
2460
2847
  //# sourceMappingURL=index.js.map
2461
2848
  //# sourceMappingURL=index.js.map